mirror of
https://github.com/mudler/LocalAI.git
synced 2026-02-03 19:22:39 -05:00
Compare commits
150 Commits
fed_refact
...
v2.19.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af0545834f | ||
|
|
c492a9735a | ||
|
|
05c75ca617 | ||
|
|
4c7e8f4d54 | ||
|
|
115b523732 | ||
|
|
4767057088 | ||
|
|
33bc1e8b19 | ||
|
|
8845524d01 | ||
|
|
92faf5fd1d | ||
|
|
2775edb3f0 | ||
|
|
98ffc00926 | ||
|
|
9b21f0d6ad | ||
|
|
57ea7f81bb | ||
|
|
274487c5eb | ||
|
|
17634b394b | ||
|
|
2d59c99d31 | ||
|
|
abcbbbed2d | ||
|
|
f1e90575f3 | ||
|
|
a7dbeb36ca | ||
|
|
d50c72a657 | ||
|
|
12b470f00a | ||
|
|
198bc6d939 | ||
|
|
3feb869025 | ||
|
|
f24fac43da | ||
|
|
9c96a73d93 | ||
|
|
45233937b7 | ||
|
|
f822bebfd8 | ||
|
|
0dd02b2ad7 | ||
|
|
9948ff2715 | ||
|
|
0da042dc2b | ||
|
|
5c747a16c4 | ||
|
|
40604e877c | ||
|
|
3dfed64a15 | ||
|
|
e5f91fbba2 | ||
|
|
4700c9df92 | ||
|
|
6f8d6f601a | ||
|
|
8a39707b36 | ||
|
|
e7df875db3 | ||
|
|
cb042713e8 | ||
|
|
7c4e526853 | ||
|
|
3a70cf311b | ||
|
|
5d08b9ac68 | ||
|
|
86f8d5b50a | ||
|
|
d4a3872dd9 | ||
|
|
d6a7a77f6b | ||
|
|
2a839e1432 | ||
|
|
610e1c00c6 | ||
|
|
b1f93935be | ||
|
|
d57acefed4 | ||
|
|
0a7e4c1b93 | ||
|
|
82cc81974f | ||
|
|
fe0d092f58 | ||
|
|
0dd21f2b5e | ||
|
|
f9fad3f4ee | ||
|
|
7021c02d45 | ||
|
|
7aa7f13095 | ||
|
|
d59bcd539e | ||
|
|
d5a6c1e4f6 | ||
|
|
7ef8edda32 | ||
|
|
81c4b72258 | ||
|
|
fe4c8c8251 | ||
|
|
02d4eeffc8 | ||
|
|
80652abc9b | ||
|
|
2169c3497d | ||
|
|
fee52942eb | ||
|
|
868182bc38 | ||
|
|
ac37b47170 | ||
|
|
43f49533e8 | ||
|
|
3379c3d98c | ||
|
|
d605df471c | ||
|
|
8bf4ccf3ed | ||
|
|
392cf15877 | ||
|
|
5eda7f578d | ||
|
|
717cc6fe1a | ||
|
|
9031d2b9eb | ||
|
|
4a69ef3052 | ||
|
|
80ae919dbe | ||
|
|
0802895cd2 | ||
|
|
9fee46207a | ||
|
|
bd900945f7 | ||
|
|
89484efaed | ||
|
|
a9757fb057 | ||
|
|
1c96e0b79e | ||
|
|
c7f0743f48 | ||
|
|
ead69a116a | ||
|
|
0314b37cd8 | ||
|
|
703cd08f01 | ||
|
|
b53947a5bb | ||
|
|
39de3cf21d | ||
|
|
e3cd11cc0a | ||
|
|
5e5037f10d | ||
|
|
9c331239d9 | ||
|
|
36789e9ead | ||
|
|
6ec593c237 | ||
|
|
bbb1dc2ae0 | ||
|
|
385d8dc29b | ||
|
|
fb574434a4 | ||
|
|
7ab3217df0 | ||
|
|
2f9f04b260 | ||
|
|
8385eb2a59 | ||
|
|
99324eeef0 | ||
|
|
ede352256b | ||
|
|
b555b64616 | ||
|
|
824cc816ea | ||
|
|
a1bc2e9771 | ||
|
|
9fc09b32cf | ||
|
|
8ec7a0a407 | ||
|
|
d3166e8571 | ||
|
|
2966979161 | ||
|
|
f4ed47bf95 | ||
|
|
1a75546b27 | ||
|
|
a6b92af875 | ||
|
|
3dc601c470 | ||
|
|
153e977155 | ||
|
|
7d61de63ae | ||
|
|
bcd9e153ba | ||
|
|
19282af059 | ||
|
|
9c0c11e8a0 | ||
|
|
3f7eddb039 | ||
|
|
77ad49333a | ||
|
|
ef5e8326c8 | ||
|
|
86509e6002 | ||
|
|
8667a67695 | ||
|
|
f505d7ab3f | ||
|
|
450dbed820 | ||
|
|
46b86f7e6e | ||
|
|
0ee1f8c1cf | ||
|
|
87bd831aba | ||
|
|
f9f83791d1 | ||
|
|
e75f73bf73 | ||
|
|
bd277162c7 | ||
|
|
f19ee465d2 | ||
|
|
7b85ff7280 | ||
|
|
134cb993c2 | ||
|
|
2cf28f3c01 | ||
|
|
18c0f4718d | ||
|
|
f878b63ee4 | ||
|
|
6eaa01db15 | ||
|
|
1d605073a4 | ||
|
|
fc29c04f82 | ||
|
|
63fc22baab | ||
|
|
6a919b30ac | ||
|
|
3f7ec2e596 | ||
|
|
82d5123c1e | ||
|
|
252961751c | ||
|
|
031627584b | ||
|
|
24a8eebcef | ||
|
|
bf9dd1de7f | ||
|
|
35d55572ac | ||
|
|
c7357a9872 |
2
.github/workflows/checksum_checker.yaml
vendored
2
.github/workflows/checksum_checker.yaml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
token: ${{ secrets.UPDATE_BOT_TOKEN }}
|
||||
push-to-fork: ci-forks/LocalAI
|
||||
commit-message: ':arrow_up: Checksum updates in gallery/index.yaml'
|
||||
title: 'models(gallery): :arrow_up: update checksum'
|
||||
title: 'chore(model-gallery): :arrow_up: update checksum'
|
||||
branch: "update/checksum"
|
||||
body: Updating checksums in gallery/index.yaml
|
||||
signoff: true
|
||||
|
||||
@@ -8,8 +8,10 @@ jobs:
|
||||
MODEL_NAME: hermes-2-theta-llama-3-8b
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: "${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fetch-depth: 0 # needed to checkout all branches for this Action to work
|
||||
- uses: mudler/localai-github-action@v1
|
||||
with:
|
||||
@@ -21,6 +23,7 @@ jobs:
|
||||
json_diff_file_output: diff.json
|
||||
raw_diff_file_output: diff.txt
|
||||
file_output_only: "true"
|
||||
base_branch: ${{ github.event.pull_request.base.sha }}
|
||||
- name: Show diff
|
||||
env:
|
||||
DIFF: ${{ steps.git-diff-action.outputs.raw-diff-path }}
|
||||
4
.github/workflows/image-pr.yml
vendored
4
.github/workflows/image-pr.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
# makeflags: "--jobs=3 --output-sync=target"
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "12"
|
||||
cuda-minor-version: "4"
|
||||
cuda-minor-version: "0"
|
||||
platforms: 'linux/amd64'
|
||||
tag-latest: 'false'
|
||||
tag-suffix: '-cublas-cuda12-ffmpeg'
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
# makeflags: "--jobs=3 --output-sync=target"
|
||||
# - build-type: 'cublas'
|
||||
# cuda-major-version: "12"
|
||||
# cuda-minor-version: "4"
|
||||
# cuda-minor-version: "0"
|
||||
# platforms: 'linux/amd64'
|
||||
# tag-latest: 'false'
|
||||
# tag-suffix: '-cublas-cuda12-ffmpeg-core'
|
||||
|
||||
8
.github/workflows/image.yml
vendored
8
.github/workflows/image.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
makeflags: "--jobs=3 --output-sync=target"
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "12"
|
||||
cuda-minor-version: "4"
|
||||
cuda-minor-version: "0"
|
||||
platforms: 'linux/amd64'
|
||||
tag-latest: 'false'
|
||||
tag-suffix: '-cublas-cuda12'
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
makeflags: "--jobs=3 --output-sync=target"
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "12"
|
||||
cuda-minor-version: "4"
|
||||
cuda-minor-version: "0"
|
||||
platforms: 'linux/amd64'
|
||||
tag-latest: 'auto'
|
||||
tag-suffix: '-cublas-cuda12-ffmpeg'
|
||||
@@ -285,7 +285,7 @@ jobs:
|
||||
makeflags: "--jobs=4 --output-sync=target"
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "12"
|
||||
cuda-minor-version: "4"
|
||||
cuda-minor-version: "0"
|
||||
platforms: 'linux/amd64'
|
||||
tag-latest: 'false'
|
||||
tag-suffix: '-cublas-cuda12-core'
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
makeflags: "--jobs=4 --output-sync=target"
|
||||
- build-type: 'cublas'
|
||||
cuda-major-version: "12"
|
||||
cuda-minor-version: "4"
|
||||
cuda-minor-version: "0"
|
||||
platforms: 'linux/amd64'
|
||||
tag-latest: 'false'
|
||||
tag-suffix: '-cublas-cuda12-ffmpeg-core'
|
||||
|
||||
9
.github/workflows/release.yaml
vendored
9
.github/workflows/release.yaml
vendored
@@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
@@ -29,11 +31,10 @@ jobs:
|
||||
with:
|
||||
go-version: '1.21.x'
|
||||
cache: false
|
||||
|
||||
- name: Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install build-essential ffmpeg protobuf-compiler ccache gawk
|
||||
sudo apt-get install build-essential ffmpeg protobuf-compiler ccache upx-ucl gawk
|
||||
sudo apt-get install -qy binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libgmock-dev
|
||||
- name: Install CUDA Dependencies
|
||||
run: |
|
||||
@@ -149,7 +150,7 @@ jobs:
|
||||
- name: Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y wget curl build-essential ffmpeg protobuf-compiler ccache gawk cmake libgmock-dev
|
||||
sudo apt-get install -y wget curl build-essential ffmpeg protobuf-compiler ccache upx-ucl gawk cmake libgmock-dev
|
||||
- name: Intel Dependencies
|
||||
run: |
|
||||
wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor | sudo tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null
|
||||
@@ -250,7 +251,7 @@ jobs:
|
||||
- name: Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends libopencv-dev protobuf-compiler ccache
|
||||
sudo apt-get install -y --no-install-recommends libopencv-dev protobuf-compiler ccache upx-ucl
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f11e97d44e567e945af
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
|
||||
- name: Build stablediffusion
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
- name: Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install build-essential curl ffmpeg
|
||||
sudo apt-get install build-essential ccache upx-ucl curl ffmpeg
|
||||
sudo apt-get install -y libgmock-dev
|
||||
curl https://repo.anaconda.com/pkgs/misc/gpgkeys/anaconda.asc | gpg --dearmor > conda.gpg && \
|
||||
sudo install -o root -g root -m 644 conda.gpg /usr/share/keyrings/conda-archive-keyring.gpg && \
|
||||
|
||||
@@ -24,7 +24,7 @@ RUN apt-get update && \
|
||||
cmake \
|
||||
curl \
|
||||
git \
|
||||
unzip && \
|
||||
unzip upx-ucl && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -99,7 +99,7 @@ FROM requirements-${IMAGE_TYPE} AS requirements-drivers
|
||||
|
||||
ARG BUILD_TYPE
|
||||
ARG CUDA_MAJOR_VERSION=12
|
||||
ARG CUDA_MINOR_VERSION=4
|
||||
ARG CUDA_MINOR_VERSION=0
|
||||
|
||||
ENV BUILD_TYPE=${BUILD_TYPE}
|
||||
|
||||
|
||||
49
Makefile
49
Makefile
@@ -8,7 +8,7 @@ DETECT_LIBS?=true
|
||||
# llama.cpp versions
|
||||
GOLLAMA_REPO?=https://github.com/go-skynet/go-llama.cpp
|
||||
GOLLAMA_VERSION?=2b57a8ae43e4699d3dc5d1496a1ccd42922993be
|
||||
CPPLLAMA_VERSION?=b3283448ce9a5098226afe1d8648ccc578511fe4
|
||||
CPPLLAMA_VERSION?=ed9d2854c9de4ae1f448334294e61167b04bec2a
|
||||
|
||||
# gpt4all version
|
||||
GPT4ALL_REPO?=https://github.com/nomic-ai/gpt4all
|
||||
@@ -20,7 +20,7 @@ RWKV_VERSION?=661e7ae26d442f5cfebd2a0881b44e8c55949ec6
|
||||
|
||||
# whisper.cpp version
|
||||
WHISPER_REPO?=https://github.com/ggerganov/whisper.cpp
|
||||
WHISPER_CPP_VERSION?=f68298ce06ca3edd6e6f3f21c3d0bb5f073942c3
|
||||
WHISPER_CPP_VERSION?=6739eb83c3ca5cf40d24c6fe8442a761a1eb6248
|
||||
|
||||
# bert.cpp version
|
||||
BERT_REPO?=https://github.com/go-skynet/go-bert.cpp
|
||||
@@ -58,7 +58,7 @@ RANDOM := $(shell bash -c 'echo $$RANDOM')
|
||||
|
||||
VERSION?=$(shell git describe --always --tags || echo "dev" )
|
||||
# go tool nm ./local-ai | grep Commit
|
||||
LD_FLAGS?=
|
||||
LD_FLAGS?=-s -w
|
||||
override LD_FLAGS += -X "github.com/mudler/LocalAI/internal.Version=$(VERSION)"
|
||||
override LD_FLAGS += -X "github.com/mudler/LocalAI/internal.Commit=$(shell git rev-parse HEAD)"
|
||||
|
||||
@@ -72,6 +72,14 @@ WHITE := $(shell tput -Txterm setaf 7)
|
||||
CYAN := $(shell tput -Txterm setaf 6)
|
||||
RESET := $(shell tput -Txterm sgr0)
|
||||
|
||||
UPX?=
|
||||
# check if upx exists
|
||||
ifeq (, $(shell which upx))
|
||||
UPX=
|
||||
else
|
||||
UPX=$(shell which upx)
|
||||
endif
|
||||
|
||||
# Default Docker bridge IP
|
||||
E2E_BRIDGE_IP?=172.17.0.1
|
||||
|
||||
@@ -377,6 +385,7 @@ build: prepare backend-assets grpcs ## Build the project
|
||||
$(info ${GREEN}I BUILD_TYPE: ${YELLOW}$(BUILD_TYPE)${RESET})
|
||||
$(info ${GREEN}I GO_TAGS: ${YELLOW}$(GO_TAGS)${RESET})
|
||||
$(info ${GREEN}I LD_FLAGS: ${YELLOW}$(LD_FLAGS)${RESET})
|
||||
$(info ${GREEN}I UPX: ${YELLOW}$(UPX)${RESET})
|
||||
ifneq ($(BACKEND_LIBS),)
|
||||
$(MAKE) backend-assets/lib
|
||||
cp -f $(BACKEND_LIBS) backend-assets/lib/
|
||||
@@ -421,7 +430,7 @@ else
|
||||
endif
|
||||
|
||||
dist-cross-linux-arm64:
|
||||
CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_NATIVE=off" GRPC_BACKENDS="backend-assets/grpc/llama-cpp-fallback backend-assets/grpc/llama-cpp-grpc backend-assets/util/llama-cpp-rpc-server" \
|
||||
CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_NATIVE=off" GRPC_BACKENDS="backend-assets/grpc/llama-cpp-fallback backend-assets/grpc/llama-cpp-grpc backend-assets/util/llama-cpp-rpc-server" GO_TAGS="p2p" \
|
||||
STATIC=true $(MAKE) build
|
||||
mkdir -p release
|
||||
# if BUILD_ID is empty, then we don't append it to the binary name
|
||||
@@ -471,7 +480,7 @@ prepare-e2e:
|
||||
mkdir -p $(TEST_DIR)
|
||||
cp -rfv $(abspath ./tests/e2e-fixtures)/gpu.yaml $(TEST_DIR)/gpu.yaml
|
||||
test -e $(TEST_DIR)/ggllm-test-model.bin || wget -q https://huggingface.co/TheBloke/CodeLlama-7B-Instruct-GGUF/resolve/main/codellama-7b-instruct.Q2_K.gguf -O $(TEST_DIR)/ggllm-test-model.bin
|
||||
docker build --build-arg GRPC_BACKENDS="$(GRPC_BACKENDS)" --build-arg IMAGE_TYPE=core --build-arg BUILD_TYPE=$(BUILD_TYPE) --build-arg CUDA_MAJOR_VERSION=12 --build-arg CUDA_MINOR_VERSION=4 --build-arg FFMPEG=true -t localai-tests .
|
||||
docker build --build-arg GRPC_BACKENDS="$(GRPC_BACKENDS)" --build-arg IMAGE_TYPE=core --build-arg BUILD_TYPE=$(BUILD_TYPE) --build-arg CUDA_MAJOR_VERSION=12 --build-arg CUDA_MINOR_VERSION=0 --build-arg FFMPEG=true -t localai-tests .
|
||||
|
||||
run-e2e-image:
|
||||
ls -liah $(abspath ./tests/e2e-fixtures)
|
||||
@@ -733,13 +742,22 @@ backend-assets/grpc: protogen-go replace
|
||||
backend-assets/grpc/bert-embeddings: sources/go-bert.cpp sources/go-bert.cpp/libgobert.a backend-assets/grpc
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS)" C_INCLUDE_PATH=$(CURDIR)/sources/go-bert.cpp LIBRARY_PATH=$(CURDIR)/sources/go-bert.cpp \
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o backend-assets/grpc/bert-embeddings ./backend/go/llm/bert/
|
||||
ifneq ($(UPX),)
|
||||
$(UPX) backend-assets/grpc/bert-embeddings
|
||||
endif
|
||||
|
||||
backend-assets/grpc/gpt4all: sources/gpt4all sources/gpt4all/gpt4all-bindings/golang/libgpt4all.a backend-assets/gpt4all backend-assets/grpc
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS)" C_INCLUDE_PATH=$(CURDIR)/sources/gpt4all/gpt4all-bindings/golang/ LIBRARY_PATH=$(CURDIR)/sources/gpt4all/gpt4all-bindings/golang/ \
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o backend-assets/grpc/gpt4all ./backend/go/llm/gpt4all/
|
||||
ifneq ($(UPX),)
|
||||
$(UPX) backend-assets/grpc/gpt4all
|
||||
endif
|
||||
|
||||
backend-assets/grpc/huggingface: backend-assets/grpc
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o backend-assets/grpc/huggingface ./backend/go/llm/langchain/
|
||||
ifneq ($(UPX),)
|
||||
$(UPX) backend-assets/grpc/huggingface
|
||||
endif
|
||||
|
||||
backend/cpp/llama/llama.cpp:
|
||||
LLAMA_VERSION=$(CPPLLAMA_VERSION) $(MAKE) -C backend/cpp/llama llama.cpp
|
||||
@@ -841,29 +859,50 @@ backend-assets/util/llama-cpp-rpc-server: backend-assets/grpc/llama-cpp-grpc
|
||||
backend-assets/grpc/llama-ggml: sources/go-llama.cpp sources/go-llama.cpp/libbinding.a backend-assets/grpc
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS)" C_INCLUDE_PATH=$(CURDIR)/sources/go-llama.cpp LIBRARY_PATH=$(CURDIR)/sources/go-llama.cpp \
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o backend-assets/grpc/llama-ggml ./backend/go/llm/llama-ggml/
|
||||
ifneq ($(UPX),)
|
||||
$(UPX) backend-assets/grpc/llama-ggml
|
||||
endif
|
||||
|
||||
backend-assets/grpc/piper: sources/go-piper sources/go-piper/libpiper_binding.a backend-assets/grpc backend-assets/espeak-ng-data
|
||||
CGO_CXXFLAGS="$(PIPER_CGO_CXXFLAGS)" CGO_LDFLAGS="$(PIPER_CGO_LDFLAGS)" LIBRARY_PATH=$(CURDIR)/sources/go-piper \
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o backend-assets/grpc/piper ./backend/go/tts/
|
||||
ifneq ($(UPX),)
|
||||
$(UPX) backend-assets/grpc/piper
|
||||
endif
|
||||
|
||||
backend-assets/grpc/rwkv: sources/go-rwkv.cpp sources/go-rwkv.cpp/librwkv.a backend-assets/grpc
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS)" C_INCLUDE_PATH=$(CURDIR)/sources/go-rwkv.cpp LIBRARY_PATH=$(CURDIR)/sources/go-rwkv.cpp \
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o backend-assets/grpc/rwkv ./backend/go/llm/rwkv
|
||||
ifneq ($(UPX),)
|
||||
$(UPX) backend-assets/grpc/rwkv
|
||||
endif
|
||||
|
||||
backend-assets/grpc/stablediffusion: sources/go-stable-diffusion sources/go-stable-diffusion/libstablediffusion.a backend-assets/grpc
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS)" CPATH="$(CPATH):$(CURDIR)/sources/go-stable-diffusion/:/usr/include/opencv4" LIBRARY_PATH=$(CURDIR)/sources/go-stable-diffusion/ \
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o backend-assets/grpc/stablediffusion ./backend/go/image/stablediffusion
|
||||
ifneq ($(UPX),)
|
||||
$(UPX) backend-assets/grpc/stablediffusion
|
||||
endif
|
||||
|
||||
backend-assets/grpc/tinydream: sources/go-tiny-dream sources/go-tiny-dream/libtinydream.a backend-assets/grpc
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS)" LIBRARY_PATH=$(CURDIR)/go-tiny-dream \
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o backend-assets/grpc/tinydream ./backend/go/image/tinydream
|
||||
ifneq ($(UPX),)
|
||||
$(UPX) backend-assets/grpc/tinydream
|
||||
endif
|
||||
|
||||
backend-assets/grpc/whisper: sources/whisper.cpp sources/whisper.cpp/libwhisper.a backend-assets/grpc
|
||||
CGO_LDFLAGS="$(CGO_LDFLAGS) $(CGO_LDFLAGS_WHISPER)" C_INCLUDE_PATH="$(CURDIR)/sources/whisper.cpp/include:$(CURDIR)/sources/whisper.cpp/ggml/include" LIBRARY_PATH=$(CURDIR)/sources/whisper.cpp \
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o backend-assets/grpc/whisper ./backend/go/transcribe/
|
||||
ifneq ($(UPX),)
|
||||
$(UPX) backend-assets/grpc/whisper
|
||||
endif
|
||||
|
||||
backend-assets/grpc/local-store: backend-assets/grpc
|
||||
$(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o backend-assets/grpc/local-store ./backend/go/stores/
|
||||
ifneq ($(UPX),)
|
||||
$(UPX) backend-assets/grpc/local-store
|
||||
endif
|
||||
|
||||
grpcs: prepare $(GRPC_BACKENDS)
|
||||
|
||||
|
||||
@@ -2259,7 +2259,6 @@ static void params_parse(const backend::ModelOptions* request,
|
||||
// get the directory of modelfile
|
||||
std::string model_dir = params.model.substr(0, params.model.find_last_of("/\\"));
|
||||
params.lora_adapter.push_back(std::make_tuple(model_dir + "/"+request->loraadapter(), scale_factor));
|
||||
params.lora_base = model_dir + "/"+request->lorabase();
|
||||
}
|
||||
params.use_mlock = request->mlock();
|
||||
params.use_mmap = request->mmap();
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
intel-extension-for-pytorch
|
||||
torch
|
||||
optimum[openvino]
|
||||
setuptools==70.3.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
setuptools==72.1.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
@@ -1,6 +1,6 @@
|
||||
accelerate
|
||||
auto-gptq==0.7.1
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
torch
|
||||
certifi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
accelerate
|
||||
bark==0.1.5
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
certifi
|
||||
transformers
|
||||
@@ -1,2 +1,2 @@
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
@@ -3,4 +3,4 @@ intel-extension-for-pytorch
|
||||
torch
|
||||
torchaudio
|
||||
optimum[openvino]
|
||||
setuptools==70.3.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
setuptools==72.1.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
@@ -1,6 +1,6 @@
|
||||
accelerate
|
||||
TTS==0.22.0
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
certifi
|
||||
transformers
|
||||
@@ -3,7 +3,7 @@ accelerate
|
||||
compel
|
||||
peft
|
||||
diffusers
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
opencv-python
|
||||
pillow
|
||||
protobuf
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
accelerate
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
certifi
|
||||
torch
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
causal-conv1d==1.4.0
|
||||
mamba-ssm==2.2.2
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
certifi
|
||||
transformers
|
||||
@@ -2,7 +2,7 @@
|
||||
intel-extension-for-pytorch
|
||||
torch
|
||||
optimum[openvino]
|
||||
grpcio==1.64.1
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
librosa==0.9.1
|
||||
faster-whisper==1.0.3
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
librosa
|
||||
faster-whisper
|
||||
|
||||
@@ -5,7 +5,7 @@ source $(dirname $0)/../common/libbackend.sh
|
||||
|
||||
# Download checkpoints if not present
|
||||
if [ ! -d "checkpoints_v2" ]; then
|
||||
wget https://myshell-public-repo-hosting.s3.amazonaws.com/openvoice/checkpoints_v2_0417.zip -O checkpoints_v2.zip
|
||||
wget https://myshell-public-repo-host.s3.amazonaws.com/openvoice/checkpoints_v2_0417.zip -O checkpoints_v2.zip
|
||||
unzip checkpoints_v2.zip
|
||||
fi
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@ intel-extension-for-pytorch
|
||||
torch
|
||||
torchaudio
|
||||
optimum[openvino]
|
||||
setuptools==70.3.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
setuptools==72.1.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
@@ -1,5 +1,5 @@
|
||||
accelerate
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
torch
|
||||
git+https://github.com/huggingface/parler-tts.git@10016fb0300c0dc31a0fb70e26f3affee7b62f16
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
intel-extension-for-pytorch
|
||||
torch
|
||||
optimum[openvino]
|
||||
setuptools==70.3.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
setuptools==72.1.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
@@ -2,4 +2,4 @@
|
||||
intel-extension-for-pytorch
|
||||
torch
|
||||
optimum[openvino]
|
||||
setuptools==70.3.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
setuptools==72.1.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
@@ -1,6 +1,6 @@
|
||||
accelerate
|
||||
rerankers[transformers]
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
certifi
|
||||
transformers
|
||||
@@ -2,4 +2,4 @@
|
||||
intel-extension-for-pytorch
|
||||
torch
|
||||
optimum[openvino]
|
||||
setuptools==70.3.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
setuptools==69.5.1 # https://github.com/mudler/LocalAI/issues/2406
|
||||
@@ -1,6 +1,6 @@
|
||||
accelerate
|
||||
sentence-transformers==3.0.1
|
||||
transformers
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
certifi
|
||||
@@ -2,4 +2,4 @@
|
||||
intel-extension-for-pytorch
|
||||
torch
|
||||
optimum[openvino]
|
||||
setuptools==70.3.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
setuptools==69.5.1 # https://github.com/mudler/LocalAI/issues/2406
|
||||
@@ -1,6 +1,6 @@
|
||||
accelerate
|
||||
transformers
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
torch
|
||||
scipy==1.14.0
|
||||
|
||||
@@ -2,4 +2,3 @@
|
||||
intel-extension-for-pytorch
|
||||
torch
|
||||
optimum[openvino]
|
||||
setuptools==70.3.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
@@ -1,9 +1,9 @@
|
||||
accelerate
|
||||
transformers
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
torch
|
||||
certifi
|
||||
intel-extension-for-transformers
|
||||
bitsandbytes
|
||||
setuptools==70.3.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
setuptools==69.5.1 # https://github.com/mudler/LocalAI/issues/2406
|
||||
|
||||
@@ -3,4 +3,4 @@ intel-extension-for-pytorch
|
||||
torch
|
||||
torchaudio
|
||||
optimum[openvino]
|
||||
setuptools==70.3.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
setuptools==72.1.0 # https://github.com/mudler/LocalAI/issues/2406
|
||||
@@ -1,4 +1,4 @@
|
||||
accelerate
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
certifi
|
||||
@@ -1,6 +1,6 @@
|
||||
accelerate
|
||||
vllm
|
||||
grpcio==1.65.0
|
||||
grpcio==1.65.1
|
||||
protobuf
|
||||
certifi
|
||||
transformers
|
||||
|
||||
@@ -10,10 +10,12 @@ import (
|
||||
type FederatedCLI struct {
|
||||
Address string `env:"LOCALAI_ADDRESS,ADDRESS" default:":8080" help:"Bind address for the API server" group:"api"`
|
||||
Peer2PeerToken string `env:"LOCALAI_P2P_TOKEN,P2P_TOKEN,TOKEN" name:"p2ptoken" help:"Token for P2P mode (optional)" group:"p2p"`
|
||||
LoadBalanced bool `env:"LOCALAI_LOAD_BALANCED,LOAD_BALANCED" default:"false" help:"Enable load balancing" group:"p2p"`
|
||||
}
|
||||
|
||||
func (f *FederatedCLI) Run(ctx *cliContext.Context) error {
|
||||
fs := p2p.NewFederatedServer(f.Address, p2p.FederatedID, f.Peer2PeerToken)
|
||||
|
||||
fs := p2p.NewFederatedServer(f.Address, p2p.FederatedID, f.Peer2PeerToken, f.LoadBalanced)
|
||||
|
||||
return fs.Start(context.Background())
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
|
||||
}
|
||||
|
||||
log.Info().Msg("Starting P2P server discovery...")
|
||||
if err := p2p.ServiceDiscoverer(context.Background(), node, token, "", func() {
|
||||
if err := p2p.ServiceDiscoverer(context.Background(), node, token, "", func(serviceID string, node p2p.NodeData) {
|
||||
var tunnelAddresses []string
|
||||
for _, v := range p2p.GetAvailableNodes("") {
|
||||
if v.IsOnline() {
|
||||
|
||||
@@ -204,35 +204,34 @@ func DeleteModelFromSystem(basePath string, name string, additionalFiles []strin
|
||||
log.Error().Err(err).Msgf("failed to read gallery file %s", configFile)
|
||||
}
|
||||
|
||||
var filesToRemove []string
|
||||
|
||||
// Remove additional files
|
||||
if galleryconfig != nil {
|
||||
for _, f := range galleryconfig.Files {
|
||||
fullPath := filepath.Join(basePath, f.Filename)
|
||||
log.Debug().Msgf("Removing file %s", fullPath)
|
||||
if e := os.Remove(fullPath); e != nil {
|
||||
err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", f.Filename, e))
|
||||
}
|
||||
filesToRemove = append(filesToRemove, fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range additionalFiles {
|
||||
fullPath := filepath.Join(filepath.Join(basePath, f))
|
||||
log.Debug().Msgf("Removing additional file %s", fullPath)
|
||||
if e := os.Remove(fullPath); e != nil {
|
||||
filesToRemove = append(filesToRemove, fullPath)
|
||||
}
|
||||
|
||||
filesToRemove = append(filesToRemove, configFile)
|
||||
filesToRemove = append(filesToRemove, galleryFile)
|
||||
|
||||
// skip duplicates
|
||||
filesToRemove = utils.Unique(filesToRemove)
|
||||
|
||||
// Removing files
|
||||
for _, f := range filesToRemove {
|
||||
if e := os.Remove(f); e != nil {
|
||||
err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", f, e))
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Removing model config file %s", configFile)
|
||||
|
||||
// Delete the model config file
|
||||
if e := os.Remove(configFile); e != nil {
|
||||
err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", configFile, e))
|
||||
}
|
||||
|
||||
// Delete gallery config file
|
||||
os.Remove(galleryFile)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/mudler/LocalAI/core/gallery"
|
||||
"github.com/mudler/LocalAI/core/p2p"
|
||||
"github.com/mudler/LocalAI/core/services"
|
||||
"github.com/mudler/LocalAI/pkg/xsync"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -372,7 +371,12 @@ func dropBadChars(s string) string {
|
||||
return strings.ReplaceAll(s, "@", "__")
|
||||
}
|
||||
|
||||
func ListModels(models []*gallery.GalleryModel, processing *xsync.SyncedMap[string, string], galleryService *services.GalleryService) string {
|
||||
type ProcessTracker interface {
|
||||
Exists(string) bool
|
||||
Get(string) string
|
||||
}
|
||||
|
||||
func ListModels(models []*gallery.GalleryModel, processTracker ProcessTracker, galleryService *services.GalleryService) string {
|
||||
modelsElements := []elem.Node{}
|
||||
descriptionDiv := func(m *gallery.GalleryModel) elem.Node {
|
||||
return elem.Div(
|
||||
@@ -396,7 +400,7 @@ func ListModels(models []*gallery.GalleryModel, processing *xsync.SyncedMap[stri
|
||||
|
||||
actionDiv := func(m *gallery.GalleryModel) elem.Node {
|
||||
galleryID := fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
|
||||
currentlyProcessing := processing.Exists(galleryID)
|
||||
currentlyProcessing := processTracker.Exists(galleryID)
|
||||
jobID := ""
|
||||
isDeletionOp := false
|
||||
if currentlyProcessing {
|
||||
@@ -404,7 +408,7 @@ func ListModels(models []*gallery.GalleryModel, processing *xsync.SyncedMap[stri
|
||||
if status != nil && status.Deletion {
|
||||
isDeletionOp = true
|
||||
}
|
||||
jobID = processing.Get(galleryID)
|
||||
jobID = processTracker.Get(galleryID)
|
||||
// TODO:
|
||||
// case not handled, if status == nil : "Waiting"
|
||||
}
|
||||
|
||||
@@ -225,18 +225,16 @@ func ChatEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, startup
|
||||
}
|
||||
|
||||
// Update input grammar
|
||||
// Handle if we should return "name" instead of "functions"
|
||||
if config.FunctionsConfig.FunctionName {
|
||||
jsStruct := funcs.ToJSONNameStructure()
|
||||
config.Grammar = jsStruct.Grammar(config.FunctionsConfig.GrammarConfig.Options()...)
|
||||
} else {
|
||||
jsStruct := funcs.ToJSONFunctionStructure()
|
||||
config.Grammar = jsStruct.Grammar(config.FunctionsConfig.GrammarConfig.Options()...)
|
||||
jsStruct := funcs.ToJSONStructure(config.FunctionsConfig.FunctionNameKey, config.FunctionsConfig.FunctionNameKey)
|
||||
g, err := jsStruct.Grammar(config.FunctionsConfig.GrammarOptions()...)
|
||||
if err == nil {
|
||||
config.Grammar = g
|
||||
}
|
||||
case input.JSONFunctionGrammarObject != nil:
|
||||
config.Grammar = input.JSONFunctionGrammarObject.Grammar(config.FunctionsConfig.GrammarConfig.Options()...)
|
||||
case input.JSONFunctionGrammarObjectName != nil:
|
||||
config.Grammar = input.JSONFunctionGrammarObjectName.Grammar(config.FunctionsConfig.GrammarConfig.Options()...)
|
||||
g, err := input.JSONFunctionGrammarObject.Grammar(config.FunctionsConfig.GrammarOptions()...)
|
||||
if err == nil {
|
||||
config.Grammar = g
|
||||
}
|
||||
default:
|
||||
// Force picking one of the functions by the request
|
||||
if config.FunctionToCall() != "" {
|
||||
|
||||
@@ -21,6 +21,40 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type modelOpCache struct {
|
||||
status *xsync.SyncedMap[string, string]
|
||||
}
|
||||
|
||||
func NewModelOpCache() *modelOpCache {
|
||||
return &modelOpCache{
|
||||
status: xsync.NewSyncedMap[string, string](),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *modelOpCache) Set(key string, value string) {
|
||||
m.status.Set(key, value)
|
||||
}
|
||||
|
||||
func (m *modelOpCache) Get(key string) string {
|
||||
return m.status.Get(key)
|
||||
}
|
||||
|
||||
func (m *modelOpCache) DeleteUUID(uuid string) {
|
||||
for _, k := range m.status.Keys() {
|
||||
if m.status.Get(k) == uuid {
|
||||
m.status.Delete(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *modelOpCache) Map() map[string]string {
|
||||
return m.status.Map()
|
||||
}
|
||||
|
||||
func (m *modelOpCache) Exists(key string) bool {
|
||||
return m.status.Exists(key)
|
||||
}
|
||||
|
||||
func RegisterUIRoutes(app *fiber.App,
|
||||
cl *config.BackendConfigLoader,
|
||||
ml *model.ModelLoader,
|
||||
@@ -29,7 +63,7 @@ func RegisterUIRoutes(app *fiber.App,
|
||||
auth func(*fiber.Ctx) error) {
|
||||
|
||||
// keeps the state of models that are being installed from the UI
|
||||
var processingModels = xsync.NewSyncedMap[string, string]()
|
||||
var processingModels = NewModelOpCache()
|
||||
|
||||
// modelStatus returns the current status of the models being processed (installation or deletion)
|
||||
// it is called asynchonously from the UI
|
||||
@@ -232,6 +266,8 @@ func RegisterUIRoutes(app *fiber.App,
|
||||
return c.SendString(elements.ProgressBar("100"))
|
||||
}
|
||||
if status.Error != nil {
|
||||
// TODO: instead of deleting the job, we should keep it in the cache and make it dismissable
|
||||
processingModels.DeleteUUID(jobUID)
|
||||
return c.SendString(elements.ErrorProgress(status.Error.Error(), status.GalleryModelName))
|
||||
}
|
||||
|
||||
@@ -246,12 +282,7 @@ func RegisterUIRoutes(app *fiber.App,
|
||||
status := galleryService.GetStatus(jobUID)
|
||||
|
||||
galleryID := ""
|
||||
for _, k := range processingModels.Keys() {
|
||||
if processingModels.Get(k) == jobUID {
|
||||
galleryID = k
|
||||
processingModels.Delete(k)
|
||||
}
|
||||
}
|
||||
processingModels.DeleteUUID(jobUID)
|
||||
if galleryID == "" {
|
||||
log.Debug().Msgf("no processing model found for job : %+v\n", jobUID)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,16 @@
|
||||
</a>
|
||||
</h2>
|
||||
<h5 class="mb-4 text-justify">LocalAI uses P2P technologies to enable distribution of work between peers. It is possible to share an instance with Federation and/or split the weights of a model across peers (only available with llama.cpp models). You can now share computational resources between your devices or your friends!</h5>
|
||||
|
||||
<!-- Warning box if p2p token is empty and p2p is enabled -->
|
||||
{{ if and .IsP2PEnabled (eq .P2PToken "") }}
|
||||
<div class="bg-red-500 p-4 rounded-lg shadow-lg mb-12 text-left">
|
||||
<p class="text-xl font-semibold text-white"> <i class="fa-solid fa-exclamation-triangle"></i> Warning: P2P mode is disabled or no token was specified</p>
|
||||
<p class="mb-4">You have to enable P2P mode by starting LocalAI with <code>--p2p</code>. Please restart the server with <code>--p2p</code> to generate a new token automatically that can be used to automatically discover other nodes. If you already have a token specify it with <code>export TOKEN=".."</code> <a href="https://localai.io/features/distribute/" target="_blank">
|
||||
Check out the documentation for more information.
|
||||
</a> </p>
|
||||
</div>
|
||||
{{ else }}
|
||||
|
||||
<!-- Federation Box -->
|
||||
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-12 text-left">
|
||||
|
||||
@@ -128,7 +137,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Llama.cpp Box END -->
|
||||
<!-- Llama.cpp Box END -->
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
47
core/p2p/federated.go
Normal file
47
core/p2p/federated.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package p2p
|
||||
|
||||
const FederatedID = "federated"
|
||||
|
||||
type FederatedServer struct {
|
||||
listenAddr, service, p2ptoken string
|
||||
requestTable map[string]int
|
||||
loadBalanced bool
|
||||
}
|
||||
|
||||
func NewFederatedServer(listenAddr, service, p2pToken string, loadBalanced bool) *FederatedServer {
|
||||
return &FederatedServer{
|
||||
listenAddr: listenAddr,
|
||||
service: service,
|
||||
p2ptoken: p2pToken,
|
||||
requestTable: map[string]int{},
|
||||
loadBalanced: loadBalanced,
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FederatedServer) SelectLeastUsedServer() string {
|
||||
// cycle over requestTable and find the entry with the lower number
|
||||
// if there are multiple entries with the same number, select one randomly
|
||||
// if there are no entries, return an empty string
|
||||
var min int
|
||||
var minKey string
|
||||
for k, v := range fs.requestTable {
|
||||
if min == 0 || v < min {
|
||||
min = v
|
||||
minKey = k
|
||||
}
|
||||
}
|
||||
return minKey
|
||||
}
|
||||
|
||||
func (fs *FederatedServer) RecordRequest(nodeID string) {
|
||||
// increment the counter for the nodeID in the requestTable
|
||||
fs.requestTable[nodeID]++
|
||||
}
|
||||
|
||||
func (fs *FederatedServer) EnsureRecordExist(nodeID string) {
|
||||
// if the nodeID is not in the requestTable, add it with a counter of 0
|
||||
_, ok := fs.requestTable[nodeID]
|
||||
if !ok {
|
||||
fs.requestTable[nodeID] = 0
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package p2p
|
||||
|
||||
type FederatedServer struct {
|
||||
listenAddr, service, p2ptoken string
|
||||
}
|
||||
|
||||
func NewFederatedServer(listenAddr, service, p2pToken string) *FederatedServer {
|
||||
return &FederatedServer{
|
||||
listenAddr: listenAddr,
|
||||
service: service,
|
||||
p2ptoken: p2pToken,
|
||||
}
|
||||
}
|
||||
@@ -7,34 +7,35 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"math/rand/v2"
|
||||
|
||||
"github.com/mudler/edgevpn/pkg/node"
|
||||
"github.com/mudler/edgevpn/pkg/protocol"
|
||||
"github.com/mudler/edgevpn/pkg/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (fs *FederatedServer) Start(ctx context.Context) error {
|
||||
n, err := NewNode(fs.p2ptoken)
|
||||
func (f *FederatedServer) Start(ctx context.Context) error {
|
||||
|
||||
n, err := NewNode(f.p2ptoken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating a new node: %w", err)
|
||||
}
|
||||
err = n.Start(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting a new node: %w", err)
|
||||
return fmt.Errorf("creating a new node: %w", err)
|
||||
}
|
||||
|
||||
if err := ServiceDiscoverer(ctx, n, fs.p2ptoken, FederatedID, nil); err != nil {
|
||||
if err := ServiceDiscoverer(ctx, n, f.p2ptoken, f.service, func(servicesID string, tunnel NodeData) {
|
||||
log.Debug().Msgf("Discovered node: %s", tunnel.ID)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fs.proxy(ctx, n)
|
||||
return f.proxy(ctx, n)
|
||||
}
|
||||
|
||||
func (fs *FederatedServer) proxy(ctx context.Context, node *node.Node) error {
|
||||
@@ -84,44 +85,56 @@ func (fs *FederatedServer) proxy(ctx context.Context, node *node.Node) error {
|
||||
}
|
||||
|
||||
// Handle connections in a new goroutine, forwarding to the p2p service
|
||||
go handleConn(conn)
|
||||
go func() {
|
||||
var tunnelAddresses []string
|
||||
for _, v := range GetAvailableNodes(fs.service) {
|
||||
if v.IsOnline() {
|
||||
tunnelAddresses = append(tunnelAddresses, v.TunnelAddress)
|
||||
} else {
|
||||
log.Info().Msgf("Node %s is offline", v.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tunnelAddresses) == 0 {
|
||||
log.Error().Msg("No available nodes yet")
|
||||
return
|
||||
}
|
||||
|
||||
tunnelAddr := ""
|
||||
|
||||
if fs.loadBalanced {
|
||||
for _, t := range tunnelAddresses {
|
||||
fs.EnsureRecordExist(t)
|
||||
}
|
||||
|
||||
tunnelAddr = fs.SelectLeastUsedServer()
|
||||
log.Debug().Msgf("Selected tunnel %s", tunnelAddr)
|
||||
if tunnelAddr == "" {
|
||||
tunnelAddr = tunnelAddresses[rand.IntN(len(tunnelAddresses))]
|
||||
}
|
||||
|
||||
fs.RecordRequest(tunnelAddr)
|
||||
} else {
|
||||
tunnelAddr = tunnelAddresses[rand.IntN(len(tunnelAddresses))]
|
||||
}
|
||||
|
||||
tunnelConn, err := net.Dial("tcp", tunnelAddr)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error connecting to tunnel")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msgf("Redirecting %s to %s", conn.LocalAddr().String(), tunnelConn.RemoteAddr().String())
|
||||
closer := make(chan struct{}, 2)
|
||||
go copyStream(closer, tunnelConn, conn)
|
||||
go copyStream(closer, conn, tunnelConn)
|
||||
<-closer
|
||||
|
||||
tunnelConn.Close()
|
||||
conn.Close()
|
||||
// ll.Infof("(service %s) Done handling %s", serviceID, l.Addr().String())
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handleConn(conn net.Conn) {
|
||||
var tunnelAddresses []string
|
||||
for _, v := range GetAvailableNodes(FederatedID) {
|
||||
if v.IsOnline() {
|
||||
tunnelAddresses = append(tunnelAddresses, v.TunnelAddress)
|
||||
} else {
|
||||
log.Info().Msgf("Node %s is offline", v.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// open a TCP stream to one of the tunnels
|
||||
// chosen randomly
|
||||
// TODO: optimize this and track usage
|
||||
tunnelAddr := tunnelAddresses[rand.IntN(len(tunnelAddresses))]
|
||||
|
||||
tunnelConn, err := net.Dial("tcp", tunnelAddr)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error connecting to tunnel")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msgf("Redirecting %s to %s", conn.LocalAddr().String(), tunnelConn.RemoteAddr().String())
|
||||
closer := make(chan struct{}, 2)
|
||||
go copyStream(closer, tunnelConn, conn)
|
||||
go copyStream(closer, conn, tunnelConn)
|
||||
<-closer
|
||||
|
||||
tunnelConn.Close()
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func copyStream(closer chan struct{}, dst io.Writer, src io.Reader) {
|
||||
defer func() { closer <- struct{}{} }() // connection is closed, send signal to stop proxy
|
||||
io.Copy(dst, src)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
)
|
||||
|
||||
const defaultServicesID = "services_localai"
|
||||
const FederatedID = "federated"
|
||||
|
||||
type NodeData struct {
|
||||
Name string
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
@@ -138,7 +139,7 @@ func allocateLocalService(ctx context.Context, node *node.Node, listenAddr, serv
|
||||
|
||||
// This is the main of the server (which keeps the env variable updated)
|
||||
// This starts a goroutine that keeps LLAMACPP_GRPC_SERVERS updated with the discovered services
|
||||
func ServiceDiscoverer(ctx context.Context, n *node.Node, token, servicesID string, discoveryFunc func()) error {
|
||||
func ServiceDiscoverer(ctx context.Context, n *node.Node, token, servicesID string, discoveryFunc func(serviceID string, node NodeData)) error {
|
||||
if servicesID == "" {
|
||||
servicesID = defaultServicesID
|
||||
}
|
||||
@@ -160,7 +161,7 @@ func ServiceDiscoverer(ctx context.Context, n *node.Node, token, servicesID stri
|
||||
case tunnel := <-tunnels:
|
||||
AddNode(servicesID, tunnel)
|
||||
if discoveryFunc != nil {
|
||||
discoveryFunc()
|
||||
discoveryFunc(servicesID, tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,3 +391,8 @@ func newNodeOpts(token string) ([]node.Option, error) {
|
||||
|
||||
return nodeOpts, nil
|
||||
}
|
||||
|
||||
func copyStream(closer chan struct{}, dst io.Writer, src io.Reader) {
|
||||
defer func() { closer <- struct{}{} }() // connection is closed, send signal to stop proxy
|
||||
io.Copy(dst, src)
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ func GenerateToken() string {
|
||||
return "not implemented"
|
||||
}
|
||||
|
||||
func (fs *FederatedServer) Start(ctx context.Context) error {
|
||||
func (f *FederatedServer) Start(ctx context.Context) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func ServiceDiscoverer(ctx context.Context, node *node.Node, token, servicesID string, fn func()) error {
|
||||
func ServiceDiscoverer(ctx context.Context, node *node.Node, token, servicesID string, fn func(string, NodeData)) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
|
||||
@@ -179,8 +179,7 @@ type OpenAIRequest struct {
|
||||
// A grammar to constrain the LLM output
|
||||
Grammar string `json:"grammar" yaml:"grammar"`
|
||||
|
||||
JSONFunctionGrammarObject *functions.JSONFunctionStructureFunction `json:"grammar_json_functions" yaml:"grammar_json_functions"`
|
||||
JSONFunctionGrammarObjectName *functions.JSONFunctionStructureName `json:"grammar_json_name" yaml:"grammar_json_name"`
|
||||
JSONFunctionGrammarObject *functions.JSONFunctionStructure `json:"grammar_json_functions" yaml:"grammar_json_functions"`
|
||||
|
||||
Backend string `json:"backend" yaml:"backend"`
|
||||
|
||||
|
||||
@@ -152,7 +152,8 @@ function:
|
||||
replace_function_results: [] # Placeholder to replace function call results with arbitrary strings or patterns.
|
||||
replace_llm_results: [] # Replace language model results with arbitrary strings or patterns.
|
||||
capture_llm_results: [] # Capture language model results as text result, among JSON, in function calls. For instance, if a model returns a block for "thinking" and a block for "response", this will allow you to capture the thinking block.
|
||||
return_name_in_function_response: false # Some models might prefer to use "name" rather then "function" when returning JSON data. This will allow to use "name" as a key in the JSON response.
|
||||
function_name_key: "name"
|
||||
function_arguments_key: "arguments"
|
||||
|
||||
# Feature gating flags to enable experimental or optional features.
|
||||
feature_flags: {}
|
||||
|
||||
@@ -5,17 +5,65 @@ weight = 15
|
||||
url = "/features/distribute/"
|
||||
+++
|
||||
|
||||
|
||||
This functionality enables LocalAI to distribute inference requests across multiple worker nodes, improving efficiency and performance. Nodes are automatically discovered and connect via p2p by using a shared token which makes sure the communication is secure and private between the nodes of the network.
|
||||
|
||||
LocalAI supports two modes of distributed inferencing via p2p:
|
||||
|
||||
- **Federated Mode**: Requests are shared between the cluster and routed to a single worker node in the network based on the load balancer's decision.
|
||||
- **Worker Mode** (aka "model sharding" or "splitting weights"): Requests are processed by all the workers which contributes to the final inference result (by sharing the model weights).
|
||||
|
||||
## Usage
|
||||
|
||||
Starting LocalAI with `--p2p` generates a shared token for connecting multiple instances: and that's all you need to create AI clusters, eliminating the need for intricate network setups.
|
||||
|
||||
Simply navigate to the "Swarm" section in the WebUI and follow the on-screen instructions.
|
||||
|
||||
For fully shared instances, initiate LocalAI with --p2p --federated and adhere to the Swarm section's guidance. This feature, while still experimental, offers a tech preview quality experience.
|
||||
|
||||
### Federated mode
|
||||
|
||||
Federated mode allows to launch multiple LocalAI instances and connect them together in a federated network. This mode is useful when you want to distribute the load of the inference across multiple nodes, but you want to have a single point of entry for the API. In the Swarm section of the WebUI, you can see the instructions to connect multiple instances together.
|
||||
|
||||

|
||||
|
||||
To start a LocalAI server in federated mode, run:
|
||||
|
||||
```bash
|
||||
local-ai run --p2p --federated
|
||||
```
|
||||
|
||||
This will generate a token that you can use to connect other LocalAI instances to the network or others can use to join the network. If you already have a token, you can specify it using the `TOKEN` environment variable.
|
||||
|
||||
To start a load balanced server that routes the requests to the network, run with the `TOKEN`:
|
||||
|
||||
```bash
|
||||
local-ai federated
|
||||
```
|
||||
|
||||
To see all the available options, run `local-ai federated --help`.
|
||||
|
||||
The instructions are displayed in the "Swarm" section of the WebUI, guiding you through the process of connecting multiple instances.
|
||||
|
||||
### Workers mode
|
||||
|
||||
{{% alert note %}}
|
||||
This feature is available exclusively with llama-cpp compatible models.
|
||||
|
||||
This feature was introduced in [LocalAI pull request #2324](https://github.com/mudler/LocalAI/pull/2324) and is based on the upstream work in [llama.cpp pull request #6829](https://github.com/ggerganov/llama.cpp/pull/6829).
|
||||
{{% /alert %}}
|
||||
|
||||
This functionality enables LocalAI to distribute inference requests across multiple worker nodes, improving efficiency and performance.
|
||||
To connect multiple workers to a single LocalAI instance, start first a server in p2p mode:
|
||||
|
||||
## Usage
|
||||
```bash
|
||||
local-ai run --p2p
|
||||
```
|
||||
|
||||
### Starting Workers
|
||||
And navigate the WebUI to the "Swarm" section to see the instructions to connect multiple workers to the network.
|
||||
|
||||

|
||||
|
||||
### Without P2P
|
||||
|
||||
To start workers for distributing the computational load, run:
|
||||
|
||||
@@ -23,48 +71,27 @@ To start workers for distributing the computational load, run:
|
||||
local-ai worker llama-cpp-rpc <listening_address> <listening_port>
|
||||
```
|
||||
|
||||
Alternatively, you can build the RPC server following the llama.cpp [README](https://github.com/ggerganov/llama.cpp/blob/master/examples/rpc/README.md), which is compatible with LocalAI.
|
||||
|
||||
### Starting LocalAI
|
||||
|
||||
To start the LocalAI server, which handles API requests, specify the worker addresses using the `LLAMACPP_GRPC_SERVERS` environment variable:
|
||||
And you can specify the address of the workers when starting LocalAI with the `LLAMACPP_GRPC_SERVERS` environment variable:
|
||||
|
||||
```bash
|
||||
LLAMACPP_GRPC_SERVERS="address1:port,address2:port" local-ai run
|
||||
```
|
||||
|
||||
The workload on the LocalAI server will then be distributed across the specified nodes.
|
||||
|
||||
## Peer-to-Peer Networking
|
||||
Alternatively, you can build the RPC workers/server following the llama.cpp [README](https://github.com/ggerganov/llama.cpp/blob/master/examples/rpc/README.md), which is compatible with LocalAI.
|
||||
|
||||

|
||||
## Manual example (worker)
|
||||
|
||||
Workers can also connect to each other in a peer-to-peer network, distributing the workload in a decentralized manner.
|
||||
|
||||
A shared token between the server and the workers is required for communication within the peer-to-peer network. This feature supports both local network (using mDNS discovery) and DHT for communication across different networks.
|
||||
|
||||
The token is automatically generated when starting the server with the `--p2p` flag. Workers can be started with the token using `local-ai worker p2p-llama-cpp-rpc` and specifying the token via the environment variable `TOKEN` or with the `--token` argument.
|
||||
|
||||
A network is established between the server and workers using DHT and mDNS discovery protocols. The llama.cpp RPC server is automatically started and exposed to the peer-to-peer network, allowing the API server to connect.
|
||||
|
||||
When the HTTP server starts, it discovers workers in the network and creates port forwards to the local service. Llama.cpp is configured to use these services. For more details on the implementation, refer to [LocalAI pull request #2343](https://github.com/mudler/LocalAI/pull/2343).
|
||||
|
||||
### Usage
|
||||
Use the WebUI to guide you in the process of starting new workers. This example shows the manual steps to highlight the process.
|
||||
|
||||
1. Start the server with `--p2p`:
|
||||
|
||||
```bash
|
||||
./local-ai run --p2p
|
||||
# 1:02AM INF loading environment variables from file envFile=.env
|
||||
# 1:02AM INF Setting logging to info
|
||||
# 1:02AM INF P2P mode enabled
|
||||
# 1:02AM INF No token provided, generating one
|
||||
# 1:02AM INF Generated Token:
|
||||
# XXXXXXXXXXX
|
||||
# 1:02AM INF Press a button to proceed
|
||||
# Get the token in the Swarm section of the WebUI
|
||||
```
|
||||
|
||||
Copy the displayed token and press Enter.
|
||||
Copy the token from the WebUI or via API call (e.g., `curl http://localhost:8000/p2p/token`) and save it for later use.
|
||||
|
||||
To reuse the same token later, restart the server with `--p2ptoken` or `P2P_TOKEN`.
|
||||
|
||||
@@ -93,11 +120,7 @@ The server logs should indicate that new workers are being discovered.
|
||||
|
||||
3. Start inference as usual on the server initiated in step 1.
|
||||
|
||||
## Notes
|
||||
|
||||
- If running in p2p mode with container images, make sure you start the container with `--net host` or `network_mode: host` in the docker-compose file.
|
||||
- Only a single model is supported currently.
|
||||
- Ensure the server detects new workers before starting inference. Currently, additional workers cannot be added once inference has begun.
|
||||

|
||||
|
||||
|
||||
## Environment Variables
|
||||
@@ -109,3 +132,20 @@ There are options that can be tweaked or parameters that can be set using enviro
|
||||
| **LOCALAI_P2P_DISABLE_DHT** | Set to "true" to disable DHT and enable p2p layer to be local only (mDNS) |
|
||||
| **LOCALAI_P2P_DISABLE_LIMITS** | Set to "true" to disable connection limits and resources management |
|
||||
| **LOCALAI_P2P_TOKEN** | Set the token for the p2p network |
|
||||
|
||||
## Architecture
|
||||
|
||||
LocalAI uses https://github.com/libp2p/go-libp2p under the hood, the same project powering IPFS. Differently from other frameworks, LocalAI uses peer2peer without a single master server, but rather it uses sub/gossip and ledger functionalities to achieve consensus across different peers.
|
||||
|
||||
[EdgeVPN](https://github.com/mudler/edgevpn) is used as a library to establish the network and expose the ledger functionality under a shared token to ease out automatic discovery and have separated, private peer2peer networks.
|
||||
|
||||
The weights are split proportional to the memory when running into worker mode, when in federation mode each request is split to every node which have to load the model fully.
|
||||
|
||||
## Notes
|
||||
|
||||
- If running in p2p mode with container images, make sure you start the container with `--net host` or `network_mode: host` in the docker-compose file.
|
||||
- Only a single model is supported currently.
|
||||
- Ensure the server detects new workers before starting inference. Currently, additional workers cannot be added once inference has begun.
|
||||
- For more details on the implementation, refer to [LocalAI pull request #2343](https://github.com/mudler/LocalAI/pull/2343)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "v2.18.1"
|
||||
"version": "v2.19.3"
|
||||
}
|
||||
|
||||
4
docs/static/install.sh
vendored
4
docs/static/install.sh
vendored
@@ -194,7 +194,7 @@ install_container_toolkit_yum() {
|
||||
curl -s -L https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | \
|
||||
$SUDO tee /etc/yum.repos.d/nvidia-container-toolkit.repo
|
||||
|
||||
if [ "$PACKAGE_MANAGER" == "dnf" ]; then
|
||||
if [ "$PACKAGE_MANAGER" = "dnf" ]; then
|
||||
$SUDO $PACKAGE_MANAGER config-manager --enable nvidia-container-toolkit-experimental
|
||||
else
|
||||
$SUDO $PACKAGE_MANAGER -y install yum-utils
|
||||
@@ -629,7 +629,7 @@ case "$ARCH" in
|
||||
*) fatal "Unsupported architecture: $ARCH" ;;
|
||||
esac
|
||||
|
||||
if [ "$OS" == "Darwin" ]; then
|
||||
if [ "$OS" = "Darwin" ]; then
|
||||
install_binary_darwin
|
||||
exit 0
|
||||
fi
|
||||
|
||||
2
docs/themes/hugo-theme-relearn
vendored
2
docs/themes/hugo-theme-relearn
vendored
Submodule docs/themes/hugo-theme-relearn updated: 1b2e139512...7aec99b38d
@@ -1,6 +1,6 @@
|
||||
llama_index==0.10.55
|
||||
llama_index==0.10.56
|
||||
requests==2.32.3
|
||||
weaviate_client==4.6.5
|
||||
weaviate_client==4.6.7
|
||||
transformers
|
||||
torch
|
||||
chainlit
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
langchain==0.2.8
|
||||
openai==1.35.13
|
||||
langchain==0.2.10
|
||||
openai==1.37.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
langchain==0.2.8
|
||||
openai==1.35.13
|
||||
chromadb==0.5.4
|
||||
llama-index==0.10.55
|
||||
langchain==0.2.10
|
||||
openai==1.37.0
|
||||
chromadb==0.5.5
|
||||
llama-index==0.10.56
|
||||
@@ -10,21 +10,21 @@ debugpy==1.8.2
|
||||
frozenlist==1.4.1
|
||||
greenlet==3.0.3
|
||||
idna==3.7
|
||||
langchain==0.2.8
|
||||
langchain-community==0.2.7
|
||||
langchain==0.2.11
|
||||
langchain-community==0.2.9
|
||||
marshmallow==3.21.3
|
||||
marshmallow-enum==1.5.1
|
||||
multidict==6.0.5
|
||||
mypy-extensions==1.0.0
|
||||
numexpr==2.10.1
|
||||
numpy==1.26.4
|
||||
openai==1.35.13
|
||||
numpy==2.0.1
|
||||
openai==1.37.1
|
||||
openapi-schema-pydantic==1.2.4
|
||||
packaging>=23.2
|
||||
pydantic==2.8.2
|
||||
PyYAML==6.0.1
|
||||
requests==2.32.3
|
||||
SQLAlchemy==2.0.30
|
||||
SQLAlchemy==2.0.31
|
||||
tenacity==8.5.0
|
||||
tqdm==4.66.4
|
||||
typing-inspect==0.9.0
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
streamlit==1.36.0
|
||||
streamlit==1.37.0
|
||||
requests
|
||||
17
gallery/alpaca.yaml
Normal file
17
gallery/alpaca.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: "alpaca"
|
||||
|
||||
config_file: |
|
||||
context_size: 4096
|
||||
f16: true
|
||||
mmap: true
|
||||
template:
|
||||
chat: |
|
||||
Below is an instruction that describes a task. Write a response that appropriately completes the request.
|
||||
|
||||
### Instruction:
|
||||
{{.Input}}
|
||||
|
||||
### Response:
|
||||
completion: |
|
||||
{{.Input}}
|
||||
1003
gallery/index.yaml
1003
gallery/index.yaml
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@ config_file: |
|
||||
{'title': 'FunctionCall', 'type': 'object', 'properties': {'arguments': {'title': 'Arguments', 'type': 'object'}, 'name': {'title': 'Name', 'type': 'string'}}, 'required': ['arguments', 'name']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
|
||||
Function call:
|
||||
chat: |
|
||||
<|begin_of_text|>{{.Input }}
|
||||
{{.Input }}
|
||||
<|start_header_id|>assistant<|end_header_id|>
|
||||
completion: |
|
||||
{{.Input}}
|
||||
|
||||
64
gallery/llama3.1-instruct-grammar.yaml
Normal file
64
gallery/llama3.1-instruct-grammar.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: "llama3-instruct-grammar"
|
||||
|
||||
config_file: |
|
||||
mmap: true
|
||||
function:
|
||||
disable_no_action: true
|
||||
grammar:
|
||||
no_mixed_free_string: true
|
||||
mixed_mode: true
|
||||
schema_type: llama3.1 # or JSON is supported too (json)
|
||||
response_regex:
|
||||
- <function=(?P<name>\w+)>(?P<arguments>.*)</function>
|
||||
template:
|
||||
chat_message: |
|
||||
<|start_header_id|>{{if eq .RoleName "assistant"}}assistant{{else if eq .RoleName "system"}}system{{else if eq .RoleName "tool"}}tool{{else if eq .RoleName "user"}}user{{end}}<|end_header_id|>
|
||||
|
||||
{{ if .FunctionCall -}}
|
||||
Function call:
|
||||
{{ else if eq .RoleName "tool" -}}
|
||||
Function response:
|
||||
{{ end -}}
|
||||
{{ if .Content -}}
|
||||
{{.Content -}}
|
||||
{{ else if .FunctionCall -}}
|
||||
{{ toJson .FunctionCall -}}
|
||||
{{ end -}}
|
||||
<|eot_id|>
|
||||
function: |
|
||||
<|start_header_id|>system<|end_header_id|>
|
||||
|
||||
You have access to the following functions:
|
||||
|
||||
{{range .Functions}}
|
||||
Use the function '{{.Name}}' to '{{.Description}}'
|
||||
{{toJson .Parameters}}
|
||||
{{end}}
|
||||
|
||||
Think very carefully before calling functions.
|
||||
If a you choose to call a function ONLY reply in the following format with no prefix or suffix:
|
||||
|
||||
<function=example_function_name>{{`{{"example_name": "example_value"}}`}}</function>
|
||||
|
||||
Reminder:
|
||||
- If looking for real time information use relevant functions before falling back to searching on internet
|
||||
- Function calls MUST follow the specified format, start with <function= and end with </function>
|
||||
- Required parameters MUST be specified
|
||||
- Only call one function at a time
|
||||
- Put the entire function call reply on one line
|
||||
<|eot_id|>
|
||||
{{.Input }}
|
||||
<|start_header_id|>assistant<|end_header_id|>
|
||||
chat: |
|
||||
{{.Input }}
|
||||
<|start_header_id|>assistant<|end_header_id|>
|
||||
completion: |
|
||||
{{.Input}}
|
||||
context_size: 8192
|
||||
f16: true
|
||||
stopwords:
|
||||
- <|im_end|>
|
||||
- <dummy32000>
|
||||
- "<|eot_id|>"
|
||||
- <|end_of_text|>
|
||||
62
gallery/llama3.1-instruct.yaml
Normal file
62
gallery/llama3.1-instruct.yaml
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: "llama3-instruct"
|
||||
|
||||
config_file: |
|
||||
mmap: true
|
||||
function:
|
||||
disable_no_action: true
|
||||
grammar:
|
||||
disable: true
|
||||
response_regex:
|
||||
- <function=(?P<name>\w+)>(?P<arguments>.*)</function>
|
||||
template:
|
||||
chat_message: |
|
||||
<|start_header_id|>{{if eq .RoleName "assistant"}}assistant{{else if eq .RoleName "system"}}system{{else if eq .RoleName "tool"}}tool{{else if eq .RoleName "user"}}user{{end}}<|end_header_id|>
|
||||
|
||||
{{ if .FunctionCall -}}
|
||||
Function call:
|
||||
{{ else if eq .RoleName "tool" -}}
|
||||
Function response:
|
||||
{{ end -}}
|
||||
{{ if .Content -}}
|
||||
{{.Content -}}
|
||||
{{ else if .FunctionCall -}}
|
||||
{{ toJson .FunctionCall -}}
|
||||
{{ end -}}
|
||||
<|eot_id|>
|
||||
function: |
|
||||
<|start_header_id|>system<|end_header_id|>
|
||||
|
||||
You have access to the following functions:
|
||||
|
||||
{{range .Functions}}
|
||||
Use the function '{{.Name}}' to '{{.Description}}'
|
||||
{{toJson .Parameters}}
|
||||
{{end}}
|
||||
|
||||
Think very carefully before calling functions.
|
||||
If a you choose to call a function ONLY reply in the following format with no prefix or suffix:
|
||||
|
||||
<function=example_function_name>{{`{{"example_name": "example_value"}}`}}</function>
|
||||
|
||||
Reminder:
|
||||
- If looking for real time information use relevant functions before falling back to searching on internet
|
||||
- Function calls MUST follow the specified format, start with <function= and end with </function>
|
||||
- Required parameters MUST be specified
|
||||
- Only call one function at a time
|
||||
- Put the entire function call reply on one line
|
||||
<|eot_id|>
|
||||
{{.Input }}
|
||||
<|start_header_id|>assistant<|end_header_id|>
|
||||
chat: |
|
||||
{{.Input }}
|
||||
<|start_header_id|>assistant<|end_header_id|>
|
||||
completion: |
|
||||
{{.Input}}
|
||||
context_size: 8192
|
||||
f16: true
|
||||
stopwords:
|
||||
- <|im_end|>
|
||||
- <dummy32000>
|
||||
- "<|eot_id|>"
|
||||
- <|end_of_text|>
|
||||
43
gallery/tuluv2.yaml
Normal file
43
gallery/tuluv2.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "tuluv2"
|
||||
|
||||
config_file: |
|
||||
mmap: true
|
||||
template:
|
||||
chat_message: |
|
||||
<|{{ .RoleName }}|>
|
||||
{{ if .FunctionCall -}}
|
||||
Function call:
|
||||
{{ else if eq .RoleName "tool" -}}
|
||||
Function response:
|
||||
{{ end -}}
|
||||
{{ if .Content -}}
|
||||
{{.Content }}
|
||||
{{ end -}}
|
||||
{{ if .FunctionCall -}}
|
||||
{{toJson .FunctionCall}}
|
||||
{{ end -}}
|
||||
function: |
|
||||
<|{{ .RoleName }}|>
|
||||
{{ if .FunctionCall -}}
|
||||
Function call:
|
||||
{{ else if eq .RoleName "tool" -}}
|
||||
Function response:
|
||||
{{ end -}}
|
||||
{{ if .Content -}}
|
||||
{{.Content }}
|
||||
{{ end -}}
|
||||
{{ if .FunctionCall -}}
|
||||
{{toJson .FunctionCall}}
|
||||
{{ end -}}
|
||||
chat: |
|
||||
{{.Input -}}
|
||||
<|assistant|>
|
||||
completion: |
|
||||
{{.Input}}
|
||||
context_size: 4096
|
||||
f16: true
|
||||
stopwords:
|
||||
- '<|im_end|>'
|
||||
- '<dummy32000>'
|
||||
- '<|endoftext|>'
|
||||
13
pkg/concurrency/concurrency_suite_test.go
Normal file
13
pkg/concurrency/concurrency_suite_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package concurrency
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestConcurrency(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Concurrency test suite")
|
||||
}
|
||||
69
pkg/concurrency/jobresult.go
Normal file
69
pkg/concurrency/jobresult.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package concurrency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// This is a Read-ONLY structure that contains the result of an arbitrary asynchronous action
|
||||
type JobResult[RequestType any, ResultType any] struct {
|
||||
request *RequestType
|
||||
result *ResultType
|
||||
err error
|
||||
once sync.Once
|
||||
done *chan struct{}
|
||||
}
|
||||
|
||||
// This structure is returned in a pair with a JobResult and serves as the structure that has access to be updated.
|
||||
type WritableJobResult[RequestType any, ResultType any] struct {
|
||||
*JobResult[RequestType, ResultType]
|
||||
}
|
||||
|
||||
// Wait blocks until the result is ready and then returns the result, or the context expires.
|
||||
// Returns *ResultType instead of ResultType since its possible we have only an error and nil for ResultType.
|
||||
// Is this correct and idiomatic?
|
||||
func (jr *JobResult[RequestType, ResultType]) Wait(ctx context.Context) (*ResultType, error) {
|
||||
if jr.done == nil { // If the channel is blanked out, result is ready.
|
||||
return jr.result, jr.err
|
||||
}
|
||||
select {
|
||||
case <-*jr.done: // Wait for the result to be ready
|
||||
jr.done = nil
|
||||
if jr.err != nil {
|
||||
return nil, jr.err
|
||||
}
|
||||
return jr.result, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Accessor function to allow holders of JobResults to access the associated request, without allowing the pointer to be updated.
|
||||
func (jr *JobResult[RequestType, ResultType]) Request() *RequestType {
|
||||
return jr.request
|
||||
}
|
||||
|
||||
// This is the function that actually updates the Result and Error on the JobResult... but it's normally not accessible
|
||||
func (jr *JobResult[RequestType, ResultType]) setResult(result ResultType, err error) {
|
||||
jr.once.Do(func() {
|
||||
jr.result = &result
|
||||
jr.err = err
|
||||
close(*jr.done) // Signal that the result is ready - since this is only ran once, jr.done cannot be set to nil yet.
|
||||
})
|
||||
}
|
||||
|
||||
// Only the WritableJobResult can actually call setResult - prevents accidental corruption
|
||||
func (wjr *WritableJobResult[RequestType, ResultType]) SetResult(result ResultType, err error) {
|
||||
wjr.JobResult.setResult(result, err)
|
||||
}
|
||||
|
||||
// NewJobResult binds a request to a matched pair of JobResult and WritableJobResult
|
||||
func NewJobResult[RequestType any, ResultType any](request RequestType) (*JobResult[RequestType, ResultType], *WritableJobResult[RequestType, ResultType]) {
|
||||
done := make(chan struct{})
|
||||
jr := &JobResult[RequestType, ResultType]{
|
||||
once: sync.Once{},
|
||||
request: &request,
|
||||
done: &done,
|
||||
}
|
||||
return jr, &WritableJobResult[RequestType, ResultType]{JobResult: jr}
|
||||
}
|
||||
80
pkg/concurrency/jobresult_test.go
Normal file
80
pkg/concurrency/jobresult_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package concurrency_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "github.com/mudler/LocalAI/pkg/concurrency"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("pkg/concurrency unit tests", func() {
|
||||
It("can be used to recieve a result across goroutines", func() {
|
||||
jr, wjr := NewJobResult[string, string]("foo")
|
||||
Expect(jr).ToNot(BeNil())
|
||||
Expect(wjr).ToNot(BeNil())
|
||||
|
||||
go func(wjr *WritableJobResult[string, string]) {
|
||||
time.Sleep(time.Second * 5)
|
||||
wjr.SetResult("bar", nil)
|
||||
}(wjr)
|
||||
|
||||
resPtr, err := jr.Wait(context.Background())
|
||||
Expect(err).To(BeNil())
|
||||
Expect(jr.Request).ToNot(BeNil())
|
||||
Expect(*jr.Request()).To(Equal("foo"))
|
||||
Expect(resPtr).ToNot(BeNil())
|
||||
Expect(*resPtr).To(Equal("bar"))
|
||||
|
||||
})
|
||||
|
||||
It("can be used to recieve an error across goroutines", func() {
|
||||
jr, wjr := NewJobResult[string, string]("foo")
|
||||
Expect(jr).ToNot(BeNil())
|
||||
Expect(wjr).ToNot(BeNil())
|
||||
|
||||
go func(wjr *WritableJobResult[string, string]) {
|
||||
time.Sleep(time.Second * 5)
|
||||
wjr.SetResult("", fmt.Errorf("test"))
|
||||
}(wjr)
|
||||
|
||||
_, err := jr.Wait(context.Background())
|
||||
Expect(jr.Request).ToNot(BeNil())
|
||||
Expect(*jr.Request()).To(Equal("foo"))
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err).To(MatchError("test"))
|
||||
})
|
||||
|
||||
It("can properly handle timeouts", func() {
|
||||
jr, wjr := NewJobResult[string, string]("foo")
|
||||
Expect(jr).ToNot(BeNil())
|
||||
Expect(wjr).ToNot(BeNil())
|
||||
|
||||
go func(wjr *WritableJobResult[string, string]) {
|
||||
time.Sleep(time.Second * 5)
|
||||
wjr.SetResult("bar", nil)
|
||||
}(wjr)
|
||||
|
||||
timeout1s, c1 := context.WithTimeoutCause(context.Background(), time.Second, fmt.Errorf("timeout"))
|
||||
timeout10s, c2 := context.WithTimeoutCause(context.Background(), time.Second*10, fmt.Errorf("timeout"))
|
||||
|
||||
_, err := jr.Wait(timeout1s)
|
||||
Expect(jr.Request).ToNot(BeNil())
|
||||
Expect(*jr.Request()).To(Equal("foo"))
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err).To(MatchError(context.DeadlineExceeded))
|
||||
|
||||
resPtr, err := jr.Wait(timeout10s)
|
||||
Expect(jr.Request).ToNot(BeNil())
|
||||
Expect(*jr.Request()).To(Equal("foo"))
|
||||
Expect(err).To(BeNil())
|
||||
Expect(resPtr).ToNot(BeNil())
|
||||
Expect(*resPtr).To(Equal("bar"))
|
||||
|
||||
// Is this needed? Cleanup Either Way.
|
||||
c1()
|
||||
c2()
|
||||
})
|
||||
})
|
||||
13
pkg/downloader/downloader_suite_test.go
Normal file
13
pkg/downloader/downloader_suite_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestDownloader(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Downloader test suite")
|
||||
}
|
||||
43
pkg/functions/function_structure.go
Normal file
43
pkg/functions/function_structure.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package functions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mudler/LocalAI/pkg/functions/grammars"
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
}
|
||||
|
||||
type JSONFunctionStructure struct {
|
||||
OneOf []Item `json:"oneOf,omitempty"`
|
||||
AnyOf []Item `json:"anyOf,omitempty"`
|
||||
Defs map[string]interface{} `json:"$defs,omitempty"`
|
||||
}
|
||||
|
||||
func (j JSONFunctionStructure) Grammar(options ...func(*grammars.GrammarOption)) (string, error) {
|
||||
grammarOpts := &grammars.GrammarOption{}
|
||||
grammarOpts.Apply(options...)
|
||||
|
||||
dat, err := json.Marshal(j)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
converter := NewSchemaConverter(*grammarOpts)
|
||||
return converter.GrammarFromBytes(dat, options...)
|
||||
}
|
||||
|
||||
type SchemaConverter interface {
|
||||
GrammarFromBytes([]byte, ...func(*grammars.GrammarOption)) (string, error)
|
||||
}
|
||||
|
||||
func NewSchemaConverter(opt grammars.GrammarOption) SchemaConverter {
|
||||
switch {
|
||||
case opt.SchemaType == grammars.LLama31Schema:
|
||||
return grammars.NewLLama31SchemaConverter(opt.FunctionName)
|
||||
}
|
||||
return grammars.NewJSONSchemaConverter(opt.PropOrder)
|
||||
}
|
||||
@@ -6,6 +6,11 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFunctionNameKey = "name"
|
||||
defaultFunctionArgumentsKey = "arguments"
|
||||
)
|
||||
|
||||
type Function struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
@@ -13,56 +18,33 @@ type Function struct {
|
||||
}
|
||||
type Functions []Function
|
||||
|
||||
type FunctionName struct {
|
||||
Const string `json:"const"`
|
||||
}
|
||||
|
||||
type Argument struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
Function Function `json:"function,omitempty"`
|
||||
}
|
||||
type Tools []Tool
|
||||
|
||||
// ToJSONFunctionStructure converts a list of functions to a JSON structure that can be parsed to a grammar
|
||||
// This allows the LLM to return a response of the type: { "function": "function_name", "arguments": { "arg1": "value1", "arg2": "value2" } }
|
||||
func (f Functions) ToJSONFunctionStructure() JSONFunctionStructureFunction {
|
||||
js := JSONFunctionStructureFunction{}
|
||||
for _, function := range f {
|
||||
// t := function.Parameters["type"]
|
||||
//tt := t.(string)
|
||||
|
||||
properties := function.Parameters["properties"]
|
||||
defs := function.Parameters["$defs"]
|
||||
dat, _ := json.Marshal(properties)
|
||||
dat2, _ := json.Marshal(defs)
|
||||
prop := map[string]interface{}{}
|
||||
defsD := map[string]interface{}{}
|
||||
|
||||
err := json.Unmarshal(dat, &prop)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error unmarshalling dat")
|
||||
}
|
||||
err = json.Unmarshal(dat2, &defsD)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error unmarshalling dat2")
|
||||
}
|
||||
if js.Defs == nil {
|
||||
js.Defs = defsD
|
||||
}
|
||||
js.OneOf = append(js.OneOf, ItemFunction{
|
||||
Type: "object",
|
||||
Properties: FunctionProperties{
|
||||
Function: FunctionName{Const: function.Name},
|
||||
Arguments: Argument{
|
||||
Type: "object",
|
||||
Properties: prop,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return js
|
||||
}
|
||||
|
||||
// ToJSONNameStructure converts a list of functions to a JSON structure that can be parsed to a grammar
|
||||
// This allows the LLM to return a response of the type: { "name": "function_name", "arguments": { "arg1": "value1", "arg2": "value2" } }
|
||||
func (f Functions) ToJSONNameStructure() JSONFunctionStructureName {
|
||||
js := JSONFunctionStructureName{}
|
||||
func (f Functions) ToJSONStructure(name, args string) JSONFunctionStructure {
|
||||
nameKey := defaultFunctionNameKey
|
||||
argsKey := defaultFunctionArgumentsKey
|
||||
if name != "" {
|
||||
nameKey = name
|
||||
}
|
||||
if args != "" {
|
||||
argsKey = args
|
||||
}
|
||||
js := JSONFunctionStructure{}
|
||||
for _, function := range f {
|
||||
// t := function.Parameters["type"]
|
||||
//tt := t.(string)
|
||||
@@ -85,15 +67,16 @@ func (f Functions) ToJSONNameStructure() JSONFunctionStructureName {
|
||||
if js.Defs == nil {
|
||||
js.Defs = defsD
|
||||
}
|
||||
js.OneOf = append(js.OneOf, ItemName{
|
||||
Type: "object",
|
||||
Properties: NameProperties{
|
||||
Function: FunctionName{Const: function.Name},
|
||||
Arguments: Argument{
|
||||
Type: "object",
|
||||
Properties: prop,
|
||||
},
|
||||
},
|
||||
|
||||
property := map[string]interface{}{}
|
||||
property[nameKey] = FunctionName{Const: function.Name}
|
||||
property[argsKey] = Argument{
|
||||
Type: "object",
|
||||
Properties: prop,
|
||||
}
|
||||
js.OneOf = append(js.OneOf, Item{
|
||||
Type: "object",
|
||||
Properties: property,
|
||||
})
|
||||
}
|
||||
return js
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package functions
|
||||
package functions_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestGrammar(t *testing.T) {
|
||||
func TestFunctions(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Grammar test suite")
|
||||
RunSpecs(t, "Functions test suite")
|
||||
}
|
||||
|
||||
@@ -35,21 +35,35 @@ var _ = Describe("LocalAI grammar functions", func() {
|
||||
},
|
||||
}
|
||||
|
||||
js := functions.ToJSONFunctionStructure()
|
||||
js := functions.ToJSONStructure("function", "arguments")
|
||||
Expect(len(js.OneOf)).To(Equal(2))
|
||||
Expect(js.OneOf[0].Properties.Function.Const).To(Equal("create_event"))
|
||||
Expect(js.OneOf[0].Properties.Arguments.Properties["event_name"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(js.OneOf[0].Properties.Arguments.Properties["event_date"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(js.OneOf[1].Properties.Function.Const).To(Equal("search"))
|
||||
Expect(js.OneOf[1].Properties.Arguments.Properties["query"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
fnName := js.OneOf[0].Properties["function"].(FunctionName)
|
||||
fnArgs := js.OneOf[0].Properties["arguments"].(Argument)
|
||||
Expect(fnName.Const).To(Equal("create_event"))
|
||||
Expect(fnArgs.Properties["event_name"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(fnArgs.Properties["event_date"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
|
||||
jsN := functions.ToJSONNameStructure()
|
||||
fnName = js.OneOf[1].Properties["function"].(FunctionName)
|
||||
fnArgs = js.OneOf[1].Properties["arguments"].(Argument)
|
||||
Expect(fnName.Const).To(Equal("search"))
|
||||
Expect(fnArgs.Properties["query"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
|
||||
// Test with custom keys
|
||||
jsN := functions.ToJSONStructure("name", "arguments")
|
||||
Expect(len(jsN.OneOf)).To(Equal(2))
|
||||
Expect(jsN.OneOf[0].Properties.Function.Const).To(Equal("create_event"))
|
||||
Expect(jsN.OneOf[0].Properties.Arguments.Properties["event_name"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(jsN.OneOf[0].Properties.Arguments.Properties["event_date"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(jsN.OneOf[1].Properties.Function.Const).To(Equal("search"))
|
||||
Expect(jsN.OneOf[1].Properties.Arguments.Properties["query"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
|
||||
fnName = jsN.OneOf[0].Properties["name"].(FunctionName)
|
||||
fnArgs = jsN.OneOf[0].Properties["arguments"].(Argument)
|
||||
|
||||
Expect(fnName.Const).To(Equal("create_event"))
|
||||
Expect(fnArgs.Properties["event_name"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
Expect(fnArgs.Properties["event_date"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
|
||||
fnName = jsN.OneOf[1].Properties["name"].(FunctionName)
|
||||
fnArgs = jsN.OneOf[1].Properties["arguments"].(Argument)
|
||||
|
||||
Expect(fnName.Const).To(Equal("search"))
|
||||
Expect(fnArgs.Properties["query"].(map[string]interface{})["type"]).To(Equal("string"))
|
||||
})
|
||||
})
|
||||
Context("Select()", func() {
|
||||
|
||||
@@ -1,406 +0,0 @@
|
||||
package functions
|
||||
|
||||
// a golang port of https://github.com/ggerganov/llama.cpp/pull/1887
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
JSONBNF = `root ::= object
|
||||
value ::= object | array | string | number | ("true" | "false" | "null") ws
|
||||
|
||||
object ::=
|
||||
"{" ws (
|
||||
string ":" ws value
|
||||
("," ws string ":" ws value)*
|
||||
)? "}" ws
|
||||
|
||||
array ::=
|
||||
"[" ws (
|
||||
value
|
||||
("," ws value)*
|
||||
)? "]" ws
|
||||
|
||||
string ::=
|
||||
"\"" (
|
||||
[^"\\] |
|
||||
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) # escapes
|
||||
)* "\"" ws
|
||||
|
||||
number ::= ("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? ws
|
||||
|
||||
ws ::= ([ \t\n] ws)?`
|
||||
)
|
||||
|
||||
var (
|
||||
SPACE_RULE = `" "?`
|
||||
|
||||
PRIMITIVE_RULES = map[string]string{
|
||||
"boolean": `("true" | "false") space`,
|
||||
"number": `("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space`,
|
||||
"integer": `("-"? ([0-9] | [1-9] [0-9]*)) space`,
|
||||
"string": `"\"" (
|
||||
[^"\\] |
|
||||
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
|
||||
)* "\"" space`,
|
||||
// TODO: we shouldn't forbid \" and \\ or all unicode and have this branch here,
|
||||
// however, if we don't have it, the grammar will be ambiguous and
|
||||
// empirically results are way worse.
|
||||
"freestring": `(
|
||||
[^\x00] |
|
||||
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
|
||||
)* space`,
|
||||
"null": `"null" space`,
|
||||
}
|
||||
|
||||
INVALID_RULE_CHARS_RE = regexp.MustCompile(`[^a-zA-Z0-9-]+`)
|
||||
GRAMMAR_LITERAL_ESCAPE_RE = regexp.MustCompile(`[\r\n"]`)
|
||||
GRAMMAR_LITERAL_ESCAPES = map[string]string{
|
||||
"\r": `\r`,
|
||||
"\n": `\n`,
|
||||
`"`: `\"`,
|
||||
}
|
||||
)
|
||||
|
||||
type JSONSchemaConverter struct {
|
||||
propOrder map[string]int
|
||||
rules map[string]string
|
||||
}
|
||||
|
||||
func NewJSONSchemaConverter(propOrder string) *JSONSchemaConverter {
|
||||
propOrderSlice := strings.Split(propOrder, ",")
|
||||
propOrderMap := make(map[string]int)
|
||||
for idx, name := range propOrderSlice {
|
||||
propOrderMap[name] = idx
|
||||
}
|
||||
|
||||
rules := make(map[string]string)
|
||||
rules["space"] = SPACE_RULE
|
||||
|
||||
return &JSONSchemaConverter{
|
||||
propOrder: propOrderMap,
|
||||
rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *JSONSchemaConverter) formatLiteral(literal interface{}) string {
|
||||
escaped := GRAMMAR_LITERAL_ESCAPE_RE.ReplaceAllStringFunc(jsonString(literal), func(match string) string {
|
||||
return GRAMMAR_LITERAL_ESCAPES[match]
|
||||
})
|
||||
return fmt.Sprintf(`"%s"`, escaped)
|
||||
}
|
||||
|
||||
func (sc *JSONSchemaConverter) addRule(name, rule string) string {
|
||||
escName := INVALID_RULE_CHARS_RE.ReplaceAllString(name, "-")
|
||||
key := escName
|
||||
if existingRule, ok := sc.rules[escName]; ok && existingRule != rule {
|
||||
i := 0
|
||||
for {
|
||||
key = fmt.Sprintf("%s%d", escName, i)
|
||||
if _, ok := sc.rules[key]; !ok {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
sc.rules[key] = rule
|
||||
return key
|
||||
}
|
||||
|
||||
const arrayNewLines = `arr ::=
|
||||
"[\n" (
|
||||
realvalue
|
||||
(",\n" realvalue)*
|
||||
)? "]"`
|
||||
|
||||
const array = `arr ::=
|
||||
"[" (
|
||||
realvalue
|
||||
("," realvalue)*
|
||||
)? "]"`
|
||||
|
||||
func (sc *JSONSchemaConverter) finalizeGrammar(options ...func(*GrammarOption)) string {
|
||||
|
||||
grammarOpts := &GrammarOption{}
|
||||
grammarOpts.Apply(options...)
|
||||
|
||||
prefix := grammarOpts.Prefix
|
||||
maybeArray := grammarOpts.MaybeArray
|
||||
disableParallelNewLines := grammarOpts.DisableParallelNewLines
|
||||
maybeString := grammarOpts.MaybeString
|
||||
noMixedFreeString := grammarOpts.NoMixedFreeString
|
||||
|
||||
var lines []string
|
||||
|
||||
swapRoot := maybeArray || maybeString || prefix != ""
|
||||
|
||||
// write down the computed rules.
|
||||
// if maybeArray is true, we need to add the array rule and slightly tweak the root rule
|
||||
for name, rule := range sc.rules {
|
||||
if swapRoot && name == "root" {
|
||||
name = "realvalue"
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("%s ::= %s", name, rule))
|
||||
}
|
||||
|
||||
if !swapRoot {
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
newRoot := "realvalue"
|
||||
if maybeArray {
|
||||
newRoot = "arr | realvalue"
|
||||
}
|
||||
|
||||
freestringRule := "mixedstring"
|
||||
if noMixedFreeString {
|
||||
freestringRule = "freestring"
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
// quote newlines in suffix
|
||||
prefix = utils.EscapeNewLines(prefix)
|
||||
|
||||
if maybeArray && maybeString {
|
||||
newRoot = "(" + newRoot + ")"
|
||||
}
|
||||
|
||||
if maybeString {
|
||||
//newRoot = "( (\"" + suffix + "\" " + newRoot + ") | freestring ) "
|
||||
newRoot = "( \"" + prefix + "\" " + newRoot + " | " + freestringRule + " ) "
|
||||
} else {
|
||||
newRoot = "\"" + prefix + "\" " + "" + newRoot + ""
|
||||
}
|
||||
} else if maybeString {
|
||||
if maybeArray {
|
||||
// newRoot = "(" + newRoot + ")"
|
||||
}
|
||||
|
||||
newRoot = freestringRule + " | " + newRoot
|
||||
}
|
||||
|
||||
lines = append(lines, fmt.Sprintf("%s ::= %s", "root", newRoot))
|
||||
if disableParallelNewLines {
|
||||
lines = append(lines, array)
|
||||
} else {
|
||||
lines = append(lines, arrayNewLines)
|
||||
}
|
||||
|
||||
if maybeArray {
|
||||
if grammarOpts.ExpectStringsAfterJSON {
|
||||
lines = append(lines, `mixedstring ::= freestring | freestring arr freestring | (freestring realvalue freestring)* | realvalue | arr`)
|
||||
} else {
|
||||
lines = append(lines, `mixedstring ::= freestring | freestring arr | freestring realvalue | realvalue | arr`)
|
||||
}
|
||||
} else {
|
||||
if grammarOpts.ExpectStringsAfterJSON {
|
||||
lines = append(lines, `mixedstring ::= freestring | (freestring realvalue freestring)* | realvalue`)
|
||||
} else {
|
||||
lines = append(lines, `mixedstring ::= freestring | freestring realvalue | realvalue`)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (sc *JSONSchemaConverter) visit(schema map[string]interface{}, name string, rootSchema map[string]interface{}) string {
|
||||
st, existType := schema["type"]
|
||||
var schemaType string
|
||||
if existType {
|
||||
schemaType = st.(string)
|
||||
}
|
||||
ruleName := name
|
||||
if name == "" {
|
||||
ruleName = "root"
|
||||
}
|
||||
_, oneOfExists := schema["oneOf"]
|
||||
_, anyOfExists := schema["anyOf"]
|
||||
if oneOfExists || anyOfExists {
|
||||
var alternatives []string
|
||||
oneOfSchemas, oneOfExists := schema["oneOf"].([]interface{})
|
||||
anyOfSchemas, anyOfExists := schema["anyOf"].([]interface{})
|
||||
|
||||
if oneOfExists {
|
||||
for i, altSchema := range oneOfSchemas {
|
||||
alternative := sc.visit(altSchema.(map[string]interface{}), fmt.Sprintf("%s-%d", ruleName, i), rootSchema)
|
||||
alternatives = append(alternatives, alternative)
|
||||
}
|
||||
} else if anyOfExists {
|
||||
for i, altSchema := range anyOfSchemas {
|
||||
alternative := sc.visit(altSchema.(map[string]interface{}), fmt.Sprintf("%s-%d", ruleName, i), rootSchema)
|
||||
alternatives = append(alternatives, alternative)
|
||||
}
|
||||
}
|
||||
|
||||
rule := strings.Join(alternatives, " | ")
|
||||
return sc.addRule(ruleName, rule)
|
||||
} else if ref, exists := schema["$ref"].(string); exists {
|
||||
referencedSchema := sc.resolveReference(ref, rootSchema)
|
||||
return sc.visit(referencedSchema, name, rootSchema)
|
||||
} else if constVal, exists := schema["const"]; exists {
|
||||
return sc.addRule(ruleName, sc.formatLiteral(constVal))
|
||||
} else if enumVals, exists := schema["enum"].([]interface{}); exists {
|
||||
var enumRules []string
|
||||
for _, enumVal := range enumVals {
|
||||
enumRule := sc.formatLiteral(enumVal)
|
||||
enumRules = append(enumRules, enumRule)
|
||||
}
|
||||
rule := strings.Join(enumRules, " | ")
|
||||
return sc.addRule(ruleName, rule)
|
||||
} else if properties, exists := schema["properties"].(map[string]interface{}); schemaType == "object" && exists {
|
||||
propOrder := sc.propOrder
|
||||
var propPairs []struct {
|
||||
propName string
|
||||
propSchema map[string]interface{}
|
||||
}
|
||||
|
||||
for propName, propSchema := range properties {
|
||||
propPairs = append(propPairs, struct {
|
||||
propName string
|
||||
propSchema map[string]interface{}
|
||||
}{propName: propName, propSchema: propSchema.(map[string]interface{})})
|
||||
}
|
||||
|
||||
sort.Slice(propPairs, func(i, j int) bool {
|
||||
iOrder := propOrder[propPairs[i].propName]
|
||||
jOrder := propOrder[propPairs[j].propName]
|
||||
if iOrder != 0 && jOrder != 0 {
|
||||
return iOrder < jOrder
|
||||
}
|
||||
return propPairs[i].propName < propPairs[j].propName
|
||||
})
|
||||
|
||||
var rule strings.Builder
|
||||
rule.WriteString(`"{" space`)
|
||||
|
||||
for i, propPair := range propPairs {
|
||||
propName := propPair.propName
|
||||
propSchema := propPair.propSchema
|
||||
propRuleName := sc.visit(propSchema, fmt.Sprintf("%s-%s", ruleName, propName), rootSchema)
|
||||
|
||||
if i > 0 {
|
||||
rule.WriteString(` "," space`)
|
||||
}
|
||||
|
||||
rule.WriteString(fmt.Sprintf(` %s space ":" space %s`, sc.formatLiteral(propName), propRuleName))
|
||||
}
|
||||
|
||||
rule.WriteString(` "}" space`)
|
||||
return sc.addRule(ruleName, rule.String())
|
||||
} else if items, exists := schema["items"].(map[string]interface{}); schemaType == "array" && exists {
|
||||
itemRuleName := sc.visit(items, fmt.Sprintf("%s-item", ruleName), rootSchema)
|
||||
rule := fmt.Sprintf(`"[" space (%s ("," space %s)*)? "]" space`, itemRuleName, itemRuleName)
|
||||
return sc.addRule(ruleName, rule)
|
||||
} else {
|
||||
primitiveRule, exists := PRIMITIVE_RULES[schemaType]
|
||||
if !exists {
|
||||
panic(fmt.Sprintf("Unrecognized schema: %v", schema))
|
||||
}
|
||||
if ruleName == "root" {
|
||||
schemaType = "root"
|
||||
}
|
||||
return sc.addRule(schemaType, primitiveRule)
|
||||
}
|
||||
}
|
||||
func (sc *JSONSchemaConverter) resolveReference(ref string, rootSchema map[string]interface{}) map[string]interface{} {
|
||||
if !strings.HasPrefix(ref, "#/$defs/") {
|
||||
panic(fmt.Sprintf("Invalid reference format: %s", ref))
|
||||
}
|
||||
|
||||
defKey := strings.TrimPrefix(ref, "#/$defs/")
|
||||
definitions, exists := rootSchema["$defs"].(map[string]interface{})
|
||||
if !exists {
|
||||
fmt.Println(rootSchema)
|
||||
|
||||
panic("No definitions found in the schema")
|
||||
}
|
||||
|
||||
def, exists := definitions[defKey].(map[string]interface{})
|
||||
if !exists {
|
||||
fmt.Println(definitions)
|
||||
|
||||
panic(fmt.Sprintf("Definition not found: %s", defKey))
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
func (sc *JSONSchemaConverter) Grammar(schema map[string]interface{}, options ...func(*GrammarOption)) string {
|
||||
sc.addRule("freestring", PRIMITIVE_RULES["freestring"])
|
||||
sc.visit(schema, "", schema)
|
||||
return sc.finalizeGrammar(options...)
|
||||
}
|
||||
|
||||
func (sc *JSONSchemaConverter) GrammarFromBytes(b []byte, options ...func(*GrammarOption)) string {
|
||||
var schema map[string]interface{}
|
||||
_ = json.Unmarshal(b, &schema)
|
||||
return sc.Grammar(schema, options...)
|
||||
}
|
||||
|
||||
func jsonString(v interface{}) string {
|
||||
b, _ := json.Marshal(v)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
type FunctionName struct {
|
||||
Const string `json:"const"`
|
||||
}
|
||||
|
||||
type FunctionProperties struct {
|
||||
Function FunctionName `json:"function"`
|
||||
Arguments Argument `json:"arguments"`
|
||||
}
|
||||
|
||||
type NameProperties struct {
|
||||
Function FunctionName `json:"name"`
|
||||
Arguments Argument `json:"arguments"`
|
||||
}
|
||||
|
||||
type Argument struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
}
|
||||
|
||||
type ItemName struct {
|
||||
Type string `json:"type"`
|
||||
Properties NameProperties `json:"properties"`
|
||||
}
|
||||
|
||||
type ItemFunction struct {
|
||||
Type string `json:"type"`
|
||||
Properties FunctionProperties `json:"properties"`
|
||||
}
|
||||
|
||||
type JSONFunctionStructureName struct {
|
||||
OneOf []ItemName `json:"oneOf,omitempty"`
|
||||
AnyOf []ItemName `json:"anyOf,omitempty"`
|
||||
Defs map[string]interface{} `json:"$defs,omitempty"`
|
||||
}
|
||||
|
||||
func (j JSONFunctionStructureName) Grammar(options ...func(*GrammarOption)) string {
|
||||
grammarOpts := &GrammarOption{}
|
||||
grammarOpts.Apply(options...)
|
||||
|
||||
dat, _ := json.Marshal(j)
|
||||
return NewJSONSchemaConverter(grammarOpts.PropOrder).GrammarFromBytes(dat, options...)
|
||||
}
|
||||
|
||||
type JSONFunctionStructureFunction struct {
|
||||
OneOf []ItemFunction `json:"oneOf,omitempty"`
|
||||
AnyOf []ItemFunction `json:"anyOf,omitempty"`
|
||||
Defs map[string]interface{} `json:"$defs,omitempty"`
|
||||
}
|
||||
|
||||
func (j JSONFunctionStructureFunction) Grammar(options ...func(*GrammarOption)) string {
|
||||
grammarOpts := &GrammarOption{}
|
||||
grammarOpts.Apply(options...)
|
||||
|
||||
dat, _ := json.Marshal(j)
|
||||
return NewJSONSchemaConverter(grammarOpts.PropOrder).GrammarFromBytes(dat, options...)
|
||||
}
|
||||
58
pkg/functions/grammars/bnf_rules.go
Normal file
58
pkg/functions/grammars/bnf_rules.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package grammars
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
PRIMITIVE_RULES = map[string]string{
|
||||
"boolean": `("true" | "false") space`,
|
||||
"number": `("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space`,
|
||||
"integer": `("-"? ([0-9] | [1-9] [0-9]*)) space`,
|
||||
"string": `"\"" (
|
||||
[^"\\] |
|
||||
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
|
||||
)* "\"" space`,
|
||||
// TODO: we shouldn't forbid \" and \\ or all unicode and have this branch here,
|
||||
// however, if we don't have it, the grammar will be ambiguous and
|
||||
// empirically results are way worse.
|
||||
"freestring": `(
|
||||
[^\x00] |
|
||||
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
|
||||
)* space`,
|
||||
"null": `"null" space`,
|
||||
}
|
||||
|
||||
INVALID_RULE_CHARS_RE = regexp.MustCompile(`[^a-zA-Z0-9-]+`)
|
||||
GRAMMAR_LITERAL_ESCAPE_RE = regexp.MustCompile(`[\r\n"]`)
|
||||
GRAMMAR_LITERAL_ESCAPES = map[string]string{
|
||||
"\r": `\r`,
|
||||
"\n": `\n`,
|
||||
`"`: `\"`,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
SPACE_RULE = `" "?`
|
||||
|
||||
arrayNewLines = `arr ::=
|
||||
"[\n" (
|
||||
realvalue
|
||||
(",\n" realvalue)*
|
||||
)? "]"`
|
||||
|
||||
array = `arr ::=
|
||||
"[" (
|
||||
realvalue
|
||||
("," realvalue)*
|
||||
)? "]"`
|
||||
)
|
||||
|
||||
func jsonString(v interface{}) (string, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
25
pkg/functions/grammars/grammars_suite_test.go
Normal file
25
pkg/functions/grammars/grammars_suite_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package grammars_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/mudler/LocalAI/pkg/functions"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestGrammar(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Grammar test suite")
|
||||
}
|
||||
|
||||
func createFunction(field1 string, field2 string, name string, properties map[string]interface{}) map[string]interface{} {
|
||||
property := map[string]interface{}{}
|
||||
property[field1] = FunctionName{Const: name}
|
||||
property[field2] = Argument{
|
||||
Type: "object",
|
||||
Properties: properties,
|
||||
}
|
||||
return property
|
||||
}
|
||||
220
pkg/functions/grammars/json_schema.go
Normal file
220
pkg/functions/grammars/json_schema.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package grammars
|
||||
|
||||
// a golang port of https://github.com/ggerganov/llama.cpp/pull/1887
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type JSONSchemaConverter struct {
|
||||
propOrder map[string]int
|
||||
rules Rules
|
||||
}
|
||||
|
||||
func NewJSONSchemaConverter(propOrder string) *JSONSchemaConverter {
|
||||
propOrderSlice := strings.Split(propOrder, ",")
|
||||
propOrderMap := make(map[string]int)
|
||||
for idx, name := range propOrderSlice {
|
||||
propOrderMap[name] = idx
|
||||
}
|
||||
|
||||
rules := make(map[string]string)
|
||||
rules["space"] = SPACE_RULE
|
||||
|
||||
return &JSONSchemaConverter{
|
||||
propOrder: propOrderMap,
|
||||
rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *JSONSchemaConverter) formatLiteral(literal interface{}) (string, error) {
|
||||
jLiteral, err := jsonString(literal)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
escaped := GRAMMAR_LITERAL_ESCAPE_RE.ReplaceAllStringFunc(jLiteral, func(match string) string {
|
||||
return GRAMMAR_LITERAL_ESCAPES[match]
|
||||
})
|
||||
return fmt.Sprintf(`"%s"`, escaped), nil
|
||||
}
|
||||
|
||||
func (sc *JSONSchemaConverter) addRule(name, rule string) string {
|
||||
escName := INVALID_RULE_CHARS_RE.ReplaceAllString(name, "-")
|
||||
key := escName
|
||||
if existingRule, ok := sc.rules[escName]; ok && existingRule != rule {
|
||||
i := 0
|
||||
for {
|
||||
key = fmt.Sprintf("%s%d", escName, i)
|
||||
if _, ok := sc.rules[key]; !ok {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
sc.rules[key] = rule
|
||||
return key
|
||||
}
|
||||
|
||||
func (sc *JSONSchemaConverter) visit(schema map[string]interface{}, name string, rootSchema map[string]interface{}) (string, error) {
|
||||
st, existType := schema["type"]
|
||||
var schemaType string
|
||||
if existType {
|
||||
schemaType = st.(string)
|
||||
}
|
||||
ruleName := name
|
||||
if name == "" {
|
||||
ruleName = "root"
|
||||
}
|
||||
_, oneOfExists := schema["oneOf"]
|
||||
_, anyOfExists := schema["anyOf"]
|
||||
if oneOfExists || anyOfExists {
|
||||
var alternatives []string
|
||||
oneOfSchemas, oneOfExists := schema["oneOf"].([]interface{})
|
||||
anyOfSchemas, anyOfExists := schema["anyOf"].([]interface{})
|
||||
|
||||
if oneOfExists {
|
||||
for i, altSchema := range oneOfSchemas {
|
||||
alternative, err := sc.visit(altSchema.(map[string]interface{}), fmt.Sprintf("%s-%d", ruleName, i), rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
alternatives = append(alternatives, alternative)
|
||||
}
|
||||
} else if anyOfExists {
|
||||
for i, altSchema := range anyOfSchemas {
|
||||
alternative, err := sc.visit(altSchema.(map[string]interface{}), fmt.Sprintf("%s-%d", ruleName, i), rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
alternatives = append(alternatives, alternative)
|
||||
}
|
||||
}
|
||||
|
||||
rule := strings.Join(alternatives, " | ")
|
||||
return sc.addRule(ruleName, rule), nil
|
||||
} else if ref, exists := schema["$ref"].(string); exists {
|
||||
referencedSchema, err := sc.resolveReference(ref, rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sc.visit(referencedSchema, name, rootSchema)
|
||||
} else if constVal, exists := schema["const"]; exists {
|
||||
literal, err := sc.formatLiteral((constVal))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sc.addRule(ruleName, literal), nil
|
||||
} else if enumVals, exists := schema["enum"].([]interface{}); exists {
|
||||
var enumRules []string
|
||||
for _, enumVal := range enumVals {
|
||||
enumRule, err := sc.formatLiteral(enumVal)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
enumRules = append(enumRules, enumRule)
|
||||
}
|
||||
rule := strings.Join(enumRules, " | ")
|
||||
return sc.addRule(ruleName, rule), nil
|
||||
} else if properties, exists := schema["properties"].(map[string]interface{}); schemaType == "object" && exists {
|
||||
propOrder := sc.propOrder
|
||||
var propPairs []struct {
|
||||
propName string
|
||||
propSchema map[string]interface{}
|
||||
}
|
||||
|
||||
for propName, propSchema := range properties {
|
||||
propPairs = append(propPairs, struct {
|
||||
propName string
|
||||
propSchema map[string]interface{}
|
||||
}{propName: propName, propSchema: propSchema.(map[string]interface{})})
|
||||
}
|
||||
|
||||
sort.Slice(propPairs, func(i, j int) bool {
|
||||
iOrder := propOrder[propPairs[i].propName]
|
||||
jOrder := propOrder[propPairs[j].propName]
|
||||
if iOrder != 0 && jOrder != 0 {
|
||||
return iOrder < jOrder
|
||||
}
|
||||
return propPairs[i].propName < propPairs[j].propName
|
||||
})
|
||||
|
||||
var rule strings.Builder
|
||||
rule.WriteString(`"{" space`)
|
||||
|
||||
for i, propPair := range propPairs {
|
||||
propName := propPair.propName
|
||||
propSchema := propPair.propSchema
|
||||
propRuleName, err := sc.visit(propSchema, fmt.Sprintf("%s-%s", ruleName, propName), rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
lPropName, err := sc.formatLiteral(propName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if i > 0 {
|
||||
rule.WriteString(` "," space`)
|
||||
}
|
||||
|
||||
rule.WriteString(fmt.Sprintf(` %s space ":" space %s`, lPropName, propRuleName))
|
||||
}
|
||||
|
||||
rule.WriteString(` "}" space`)
|
||||
return sc.addRule(ruleName, rule.String()), nil
|
||||
} else if items, exists := schema["items"].(map[string]interface{}); schemaType == "array" && exists {
|
||||
itemRuleName, err := sc.visit(items, fmt.Sprintf("%s-item", ruleName), rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rule := fmt.Sprintf(`"[" space (%s ("," space %s)*)? "]" space`, itemRuleName, itemRuleName)
|
||||
return sc.addRule(ruleName, rule), nil
|
||||
} else {
|
||||
primitiveRule, exists := PRIMITIVE_RULES[schemaType]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("unrecognized schema: %v", schema)
|
||||
}
|
||||
if ruleName == "root" {
|
||||
schemaType = "root"
|
||||
}
|
||||
return sc.addRule(schemaType, primitiveRule), nil
|
||||
}
|
||||
}
|
||||
func (sc *JSONSchemaConverter) resolveReference(ref string, rootSchema map[string]interface{}) (map[string]interface{}, error) {
|
||||
if !strings.HasPrefix(ref, "#/$defs/") {
|
||||
return nil, fmt.Errorf("invalid reference format: %s", ref)
|
||||
}
|
||||
|
||||
defKey := strings.TrimPrefix(ref, "#/$defs/")
|
||||
definitions, exists := rootSchema["$defs"].(map[string]interface{})
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no definitions found in the schema: %s", rootSchema)
|
||||
}
|
||||
|
||||
def, exists := definitions[defKey].(map[string]interface{})
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("definition not found: %s %+v", defKey, definitions)
|
||||
}
|
||||
|
||||
return def, nil
|
||||
}
|
||||
|
||||
func (sc *JSONSchemaConverter) Grammar(schema map[string]interface{}, options ...func(*GrammarOption)) (string, error) {
|
||||
sc.addRule("freestring", PRIMITIVE_RULES["freestring"])
|
||||
_, err := sc.visit(schema, "", schema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sc.rules.ToGrammar(options...), nil
|
||||
}
|
||||
|
||||
func (sc *JSONSchemaConverter) GrammarFromBytes(b []byte, options ...func(*GrammarOption)) (string, error) {
|
||||
var schema map[string]interface{}
|
||||
err := json.Unmarshal(b, &schema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sc.Grammar(schema, options...)
|
||||
}
|
||||
@@ -1,77 +1,63 @@
|
||||
package functions_test
|
||||
package grammars_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAI/pkg/functions"
|
||||
. "github.com/mudler/LocalAI/pkg/functions"
|
||||
. "github.com/mudler/LocalAI/pkg/functions/grammars"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var testFunctions = []ItemFunction{
|
||||
var testFunctions = []Item{
|
||||
{
|
||||
Type: "object",
|
||||
Properties: FunctionProperties{
|
||||
Function: FunctionName{
|
||||
Const: "create_event",
|
||||
Properties: createFunction(
|
||||
"function",
|
||||
"arguments",
|
||||
"create_event",
|
||||
map[string]interface{}{
|
||||
"title": map[string]string{"type": "string"},
|
||||
"date": map[string]string{"type": "string"},
|
||||
"time": map[string]string{"type": "string"},
|
||||
},
|
||||
Arguments: Argument{ // this is OpenAI's parameter
|
||||
Type: "object",
|
||||
Properties: map[string]interface{}{
|
||||
"title": map[string]string{"type": "string"},
|
||||
"date": map[string]string{"type": "string"},
|
||||
"time": map[string]string{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
Type: "object",
|
||||
Properties: FunctionProperties{
|
||||
Function: FunctionName{
|
||||
Const: "search",
|
||||
},
|
||||
Arguments: Argument{
|
||||
Type: "object",
|
||||
Properties: map[string]interface{}{
|
||||
"query": map[string]string{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: createFunction(
|
||||
"function",
|
||||
"arguments",
|
||||
"search",
|
||||
map[string]interface{}{
|
||||
"query": map[string]string{"type": "string"},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
var testFunctionsName = []ItemName{
|
||||
var testFunctionsName = []Item{
|
||||
{
|
||||
Type: "object",
|
||||
Properties: NameProperties{
|
||||
Function: FunctionName{
|
||||
Const: "create_event",
|
||||
Properties: createFunction(
|
||||
"name",
|
||||
"arguments",
|
||||
"create_event",
|
||||
map[string]interface{}{
|
||||
"title": map[string]string{"type": "string"},
|
||||
"date": map[string]string{"type": "string"},
|
||||
"time": map[string]string{"type": "string"},
|
||||
},
|
||||
Arguments: Argument{ // this is OpenAI's parameter
|
||||
Type: "object",
|
||||
Properties: map[string]interface{}{
|
||||
"title": map[string]string{"type": "string"},
|
||||
"date": map[string]string{"type": "string"},
|
||||
"time": map[string]string{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
Type: "object",
|
||||
Properties: NameProperties{
|
||||
Function: FunctionName{
|
||||
Const: "search",
|
||||
},
|
||||
Arguments: Argument{
|
||||
Type: "object",
|
||||
Properties: map[string]interface{}{
|
||||
"query": map[string]string{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: createFunction(
|
||||
"name",
|
||||
"arguments",
|
||||
"search",
|
||||
map[string]interface{}{
|
||||
"query": map[string]string{"type": "string"},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -249,7 +235,8 @@ root-1-name ::= "\"search\""`
|
||||
var _ = Describe("JSON schema grammar tests", func() {
|
||||
Context("JSON", func() {
|
||||
It("generates a valid grammar from JSON schema", func() {
|
||||
grammar := NewJSONSchemaConverter("").GrammarFromBytes([]byte(testInput1))
|
||||
grammar, err := NewJSONSchemaConverter("").GrammarFromBytes([]byte(testInput1))
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(inputResult1, "\n")
|
||||
for _, r := range results {
|
||||
if r != "" {
|
||||
@@ -259,7 +246,8 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))))
|
||||
})
|
||||
It("generates a valid grammar from JSON schema", func() {
|
||||
grammar := NewJSONSchemaConverter("").GrammarFromBytes([]byte(testInput2))
|
||||
grammar, err := NewJSONSchemaConverter("").GrammarFromBytes([]byte(testInput2))
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(inputResult3, "\n")
|
||||
for _, r := range results {
|
||||
if r != "" {
|
||||
@@ -270,10 +258,11 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
It("generates a valid grammar from JSON Objects", func() {
|
||||
|
||||
structuredGrammar := JSONFunctionStructureFunction{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctions}
|
||||
|
||||
grammar := structuredGrammar.Grammar()
|
||||
grammar, err := structuredGrammar.Grammar()
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(inputResult1, "\n")
|
||||
for _, r := range results {
|
||||
if r != "" {
|
||||
@@ -284,10 +273,11 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
It("generates a valid grammar from JSON Objects for multiple function return", func() {
|
||||
structuredGrammar := JSONFunctionStructureFunction{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctions}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.EnableMaybeArray)
|
||||
grammar, err := structuredGrammar.Grammar(EnableMaybeArray)
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(
|
||||
strings.Join([]string{
|
||||
inputResult2,
|
||||
@@ -302,10 +292,11 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
It("generates a valid grammar from JSON Objects for multiple function return", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.EnableMaybeArray)
|
||||
grammar, err := structuredGrammar.Grammar(EnableMaybeArray)
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(
|
||||
strings.Join([]string{
|
||||
inputResult4,
|
||||
@@ -320,13 +311,14 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
It("generates a valid grammar from JSON Objects for multiple function return with a suffix and array", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(
|
||||
functions.SetPrefix("suffix"),
|
||||
functions.EnableMaybeArray,
|
||||
grammar, err := structuredGrammar.Grammar(
|
||||
SetPrefix("suffix"),
|
||||
EnableMaybeArray,
|
||||
)
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(
|
||||
strings.Join([]string{
|
||||
rootResult(`"suffix" arr | realvalue`),
|
||||
@@ -340,10 +332,11 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar)
|
||||
})
|
||||
It("generates a valid grammar from JSON Objects with a suffix", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.SetPrefix("suffix"))
|
||||
grammar, err := structuredGrammar.Grammar(SetPrefix("suffix"))
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(
|
||||
strings.Join([]string{
|
||||
rootResult(`"suffix" realvalue`),
|
||||
@@ -357,10 +350,11 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar)
|
||||
})
|
||||
It("generates a valid grammar from JSON Objects with a suffix and could return string", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.SetPrefix("suffix"), functions.EnableMaybeString)
|
||||
grammar, err := structuredGrammar.Grammar(SetPrefix("suffix"), EnableMaybeString)
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(
|
||||
strings.Join([]string{
|
||||
rootResult(`( "suffix" realvalue | mixedstring )`),
|
||||
@@ -374,10 +368,11 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))), grammar)
|
||||
})
|
||||
It("generates a valid grammar from JSON Objects with a suffix that could return text or an array of tools", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.SetPrefix("suffix"), functions.EnableMaybeString, functions.EnableMaybeArray)
|
||||
grammar, err := structuredGrammar.Grammar(SetPrefix("suffix"), EnableMaybeString, EnableMaybeArray)
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(
|
||||
strings.Join([]string{
|
||||
rootResult(`( "suffix" (arr | realvalue) | mixedstring )`),
|
||||
@@ -393,10 +388,11 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
It("generates a valid grammar from JSON Objects without a suffix that could return text or an array of tools or just string", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.EnableMaybeString, functions.EnableMaybeArray)
|
||||
grammar, err := structuredGrammar.Grammar(EnableMaybeString, EnableMaybeArray)
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(
|
||||
strings.Join([]string{
|
||||
rootResult(`mixedstring | arr | realvalue`),
|
||||
@@ -411,10 +407,11 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
It("generates a valid grammar from JSON Objects without a suffix that could return text or an array of tools or just string. Disables mixedstring", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
|
||||
grammar := structuredGrammar.Grammar(functions.EnableMaybeString, functions.EnableMaybeArray, functions.NoMixedFreeString)
|
||||
grammar, err := structuredGrammar.Grammar(EnableMaybeString, EnableMaybeArray, NoMixedFreeString)
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(
|
||||
strings.Join([]string{
|
||||
rootResult(`freestring | arr | realvalue`),
|
||||
@@ -429,14 +426,15 @@ var _ = Describe("JSON schema grammar tests", func() {
|
||||
})
|
||||
|
||||
It("generates parallel tools without newlines in JSON", func() {
|
||||
structuredGrammar := JSONFunctionStructureName{
|
||||
structuredGrammar := JSONFunctionStructure{
|
||||
OneOf: testFunctionsName}
|
||||
content := `arr ::=
|
||||
"[" (
|
||||
realvalue
|
||||
("," realvalue)*
|
||||
)? "]"`
|
||||
grammar := structuredGrammar.Grammar(functions.EnableMaybeString, functions.EnableMaybeArray, functions.DisableParallelNewLines)
|
||||
grammar, err := structuredGrammar.Grammar(EnableMaybeString, EnableMaybeArray, DisableParallelNewLines)
|
||||
Expect(err).To(BeNil())
|
||||
results := strings.Split(content, "\n")
|
||||
for _, r := range results {
|
||||
if r != "" {
|
||||
281
pkg/functions/grammars/llama31_schema.go
Normal file
281
pkg/functions/grammars/llama31_schema.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package grammars
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LLama31SchemaConverter struct {
|
||||
fnName string
|
||||
rules Rules
|
||||
}
|
||||
|
||||
func NewLLama31SchemaConverter(fnName string) *LLama31SchemaConverter {
|
||||
rules := make(map[string]string)
|
||||
rules["space"] = SPACE_RULE
|
||||
if fnName == "" {
|
||||
fnName = "name"
|
||||
}
|
||||
|
||||
return &LLama31SchemaConverter{
|
||||
rules: rules,
|
||||
fnName: fnName,
|
||||
}
|
||||
}
|
||||
|
||||
var GRAMMAR_LITERAL_ESCAPESLlama = map[string]string{
|
||||
"\r": `\r`,
|
||||
"\n": `\n`,
|
||||
}
|
||||
|
||||
var GRAMMAR_LITERAL_ESCAPE_RELlama = regexp.MustCompile(`[\r\n]`)
|
||||
|
||||
func (sc *LLama31SchemaConverter) formatLiteral(literal interface{}) (string, error) {
|
||||
jLiteral, err := jsonString(literal)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
escaped := GRAMMAR_LITERAL_ESCAPE_RELlama.ReplaceAllStringFunc(jLiteral, func(match string) string {
|
||||
return GRAMMAR_LITERAL_ESCAPESLlama[match]
|
||||
})
|
||||
return escaped, nil
|
||||
}
|
||||
|
||||
func (sc *LLama31SchemaConverter) formatLiteralQuoted(literal interface{}) (string, error) {
|
||||
jLiteral, err := jsonString(literal)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
escaped := GRAMMAR_LITERAL_ESCAPE_RE.ReplaceAllStringFunc(jLiteral, func(match string) string {
|
||||
return GRAMMAR_LITERAL_ESCAPES[match]
|
||||
})
|
||||
return fmt.Sprintf(`"%s"`, escaped), nil
|
||||
}
|
||||
|
||||
func (sc *LLama31SchemaConverter) addRule(name, rule string) string {
|
||||
escName := INVALID_RULE_CHARS_RE.ReplaceAllString(name, "-")
|
||||
key := escName
|
||||
if existingRule, ok := sc.rules[escName]; ok && existingRule != rule {
|
||||
i := 0
|
||||
for {
|
||||
key = fmt.Sprintf("%s%d", escName, i)
|
||||
if _, ok := sc.rules[key]; !ok {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
sc.rules[key] = rule
|
||||
return key
|
||||
}
|
||||
|
||||
func (sc *LLama31SchemaConverter) visit(schema map[string]interface{}, name string, rootSchema map[string]interface{}) (string, error) {
|
||||
st, existType := schema["type"]
|
||||
var schemaType string
|
||||
if existType {
|
||||
schemaType = st.(string)
|
||||
}
|
||||
ruleName := name
|
||||
if name == "" {
|
||||
ruleName = "root"
|
||||
}
|
||||
_, oneOfExists := schema["oneOf"]
|
||||
_, anyOfExists := schema["anyOf"]
|
||||
if oneOfExists || anyOfExists {
|
||||
var alternatives []string
|
||||
oneOfSchemas, oneOfExists := schema["oneOf"].([]interface{})
|
||||
anyOfSchemas, anyOfExists := schema["anyOf"].([]interface{})
|
||||
|
||||
if oneOfExists {
|
||||
for i, altSchema := range oneOfSchemas {
|
||||
alternative, err := sc.visit(altSchema.(map[string]interface{}), fmt.Sprintf("%s-%d", ruleName, i), rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
alternatives = append(alternatives, alternative)
|
||||
}
|
||||
} else if anyOfExists {
|
||||
for i, altSchema := range anyOfSchemas {
|
||||
alternative, err := sc.visit(altSchema.(map[string]interface{}), fmt.Sprintf("%s-%d", ruleName, i), rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
alternatives = append(alternatives, alternative)
|
||||
}
|
||||
}
|
||||
|
||||
rule := strings.Join(alternatives, " | ")
|
||||
return sc.addRule(ruleName, rule), nil
|
||||
} else if ref, exists := schema["$ref"].(string); exists {
|
||||
referencedSchema, err := sc.resolveReference(ref, rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sc.visit(referencedSchema, name, rootSchema)
|
||||
} else if constVal, exists := schema["const"]; exists {
|
||||
|
||||
literal, err := sc.formatLiteral((constVal))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sc.addRule(ruleName, literal), nil
|
||||
} else if enumVals, exists := schema["enum"].([]interface{}); exists {
|
||||
var enumRules []string
|
||||
for _, enumVal := range enumVals {
|
||||
enumRule, err := sc.formatLiteralQuoted(enumVal)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
enumRules = append(enumRules, enumRule)
|
||||
}
|
||||
rule := strings.Join(enumRules, " | ")
|
||||
return sc.addRule(ruleName, rule), nil
|
||||
} else if properties, exists := schema["properties"].(map[string]interface{}); schemaType == "object" && exists {
|
||||
baseProperty := false
|
||||
depth := strings.Split(name, "-")
|
||||
if len(depth) == 2 {
|
||||
baseProperty = true
|
||||
}
|
||||
type propData []struct {
|
||||
propName string
|
||||
propSchema map[string]interface{}
|
||||
}
|
||||
var propPairs propData
|
||||
|
||||
for propName, propSchema := range properties {
|
||||
propPairs = append(propPairs, struct {
|
||||
propName string
|
||||
propSchema map[string]interface{}
|
||||
}{propName: propName, propSchema: propSchema.(map[string]interface{})})
|
||||
}
|
||||
|
||||
sort.Slice(propPairs, func(i, j int) bool {
|
||||
return propPairs[i].propName < propPairs[j].propName
|
||||
})
|
||||
|
||||
var rule strings.Builder
|
||||
if baseProperty {
|
||||
rule.WriteString(`"<function="`)
|
||||
} else {
|
||||
rule.WriteString(`"{" space`)
|
||||
}
|
||||
|
||||
if baseProperty {
|
||||
|
||||
namePair := propData{}
|
||||
for i, propPair := range propPairs {
|
||||
propName := propPair.propName
|
||||
if propName == sc.fnName {
|
||||
namePair = append(namePair, propPair)
|
||||
// remove namePair from propPairs
|
||||
propPairs = append(propPairs[:i], propPairs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(namePair) == 0 {
|
||||
return "", fmt.Errorf("no function name found in the schema: %s", schema)
|
||||
}
|
||||
|
||||
propRuleName, err := sc.visit(namePair[0].propSchema, fmt.Sprintf("%s-%s", ruleName, sc.fnName), rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rule.WriteString(fmt.Sprintf(` %s ">{" `, propRuleName))
|
||||
|
||||
for _, propPair := range propPairs {
|
||||
propName := propPair.propName
|
||||
propSchema := propPair.propSchema
|
||||
propRuleName, err := sc.visit(propSchema, fmt.Sprintf("%s-%s", ruleName, propName), rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rule.WriteString(propRuleName)
|
||||
}
|
||||
|
||||
rule.WriteString(` "}</function>"`)
|
||||
|
||||
} else {
|
||||
for i, propPair := range propPairs {
|
||||
propName := propPair.propName
|
||||
propSchema := propPair.propSchema
|
||||
propRuleName, err := sc.visit(propSchema, fmt.Sprintf("%s-%s", ruleName, propName), rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
lPropName, err := sc.formatLiteralQuoted(propName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if i > 0 {
|
||||
rule.WriteString(` "," space`)
|
||||
}
|
||||
|
||||
rule.WriteString(fmt.Sprintf(` %s space ":" space %s`, lPropName, propRuleName))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !baseProperty {
|
||||
rule.WriteString(` "}" space`)
|
||||
}
|
||||
|
||||
return sc.addRule(ruleName, rule.String()), nil
|
||||
} else if items, exists := schema["items"].(map[string]interface{}); schemaType == "array" && exists {
|
||||
itemRuleName, err := sc.visit(items, fmt.Sprintf("%s-item", ruleName), rootSchema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rule := fmt.Sprintf(`"[" space (%s ("," space %s)*)? "]" space`, itemRuleName, itemRuleName)
|
||||
return sc.addRule(ruleName, rule), nil
|
||||
} else {
|
||||
primitiveRule, exists := PRIMITIVE_RULES[schemaType]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("unrecognized schema: %v", schema)
|
||||
}
|
||||
if ruleName == "root" {
|
||||
schemaType = "root"
|
||||
}
|
||||
return sc.addRule(schemaType, primitiveRule), nil
|
||||
}
|
||||
}
|
||||
func (sc *LLama31SchemaConverter) resolveReference(ref string, rootSchema map[string]interface{}) (map[string]interface{}, error) {
|
||||
if !strings.HasPrefix(ref, "#/$defs/") {
|
||||
return nil, fmt.Errorf("invalid reference format: %s", ref)
|
||||
}
|
||||
|
||||
defKey := strings.TrimPrefix(ref, "#/$defs/")
|
||||
definitions, exists := rootSchema["$defs"].(map[string]interface{})
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no definitions found in the schema: %s", rootSchema)
|
||||
}
|
||||
|
||||
def, exists := definitions[defKey].(map[string]interface{})
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("definition not found: %s %+v", defKey, definitions)
|
||||
}
|
||||
|
||||
return def, nil
|
||||
}
|
||||
|
||||
func (sc *LLama31SchemaConverter) Grammar(schema map[string]interface{}, options ...func(*GrammarOption)) (string, error) {
|
||||
sc.addRule("freestring", PRIMITIVE_RULES["freestring"])
|
||||
_, err := sc.visit(schema, "", schema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sc.rules.ToGrammar(options...), nil
|
||||
}
|
||||
|
||||
func (sc *LLama31SchemaConverter) GrammarFromBytes(b []byte, options ...func(*GrammarOption)) (string, error) {
|
||||
var schema map[string]interface{}
|
||||
err := json.Unmarshal(b, &schema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sc.Grammar(schema, options...)
|
||||
}
|
||||
76
pkg/functions/grammars/llama31_schema_test.go
Normal file
76
pkg/functions/grammars/llama31_schema_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package grammars_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
. "github.com/mudler/LocalAI/pkg/functions/grammars"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const (
|
||||
testllama31Input1 = `
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"function": {"const": "create_event"},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"date": {"type": "string"},
|
||||
"time": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"function": {"const": "search"},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
// <function=example_function_name>{{"example_name": "example_value"}}</function>
|
||||
testllama31inputResult1 = `root-0-function ::= "create_event"
|
||||
freestring ::= (
|
||||
[^"\\] |
|
||||
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
|
||||
)* space
|
||||
root-0 ::= "<function=" root-0-function ">{" root-0-arguments "}</function>"
|
||||
root-1-arguments ::= "{" space "\"query\"" space ":" space string "}" space
|
||||
root ::= root-0 | root-1
|
||||
space ::= " "?
|
||||
root-0-arguments ::= "{" space "\"date\"" space ":" space string "," space "\"time\"" space ":" space string "," space "\"title\"" space ":" space string "}" space
|
||||
root-1 ::= "<function=" root-1-function ">{" root-1-arguments "}</function>"
|
||||
string ::= "\"" (
|
||||
[^"\\] |
|
||||
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
|
||||
)* "\"" space
|
||||
root-1-function ::= "search"`
|
||||
)
|
||||
|
||||
var _ = Describe("JSON schema grammar tests", func() {
|
||||
Context("JSON", func() {
|
||||
It("generates a valid grammar from JSON schema", func() {
|
||||
grammar, err := NewLLama31SchemaConverter("function").GrammarFromBytes([]byte(testllama31Input1))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
results := strings.Split(testllama31inputResult1, "\n")
|
||||
for _, r := range results {
|
||||
if r != "" {
|
||||
Expect(grammar).To(ContainSubstring(r))
|
||||
}
|
||||
}
|
||||
Expect(len(results)).To(Equal(len(strings.Split(grammar, "\n"))))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
package functions
|
||||
package grammars
|
||||
|
||||
type GrammarOption struct {
|
||||
PropOrder string
|
||||
@@ -8,6 +8,9 @@ type GrammarOption struct {
|
||||
MaybeString bool
|
||||
NoMixedFreeString bool
|
||||
ExpectStringsAfterJSON bool
|
||||
|
||||
FunctionName string
|
||||
SchemaType SchemaConverterType
|
||||
}
|
||||
|
||||
func (o *GrammarOption) Apply(options ...func(*GrammarOption)) {
|
||||
@@ -48,3 +51,15 @@ func SetPropOrder(order string) func(*GrammarOption) {
|
||||
o.PropOrder = order
|
||||
}
|
||||
}
|
||||
|
||||
func WithSchemaType(schemaType SchemaConverterType) func(*GrammarOption) {
|
||||
return func(o *GrammarOption) {
|
||||
o.SchemaType = schemaType
|
||||
}
|
||||
}
|
||||
|
||||
func WithFunctionName(name string) func(*GrammarOption) {
|
||||
return func(o *GrammarOption) {
|
||||
o.FunctionName = name
|
||||
}
|
||||
}
|
||||
93
pkg/functions/grammars/rules.go
Normal file
93
pkg/functions/grammars/rules.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package grammars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
)
|
||||
|
||||
type Rules map[string]string
|
||||
|
||||
func (rules Rules) ToGrammar(options ...func(*GrammarOption)) string {
|
||||
grammarOpts := &GrammarOption{}
|
||||
grammarOpts.Apply(options...)
|
||||
|
||||
prefix := grammarOpts.Prefix
|
||||
maybeArray := grammarOpts.MaybeArray
|
||||
disableParallelNewLines := grammarOpts.DisableParallelNewLines
|
||||
maybeString := grammarOpts.MaybeString
|
||||
noMixedFreeString := grammarOpts.NoMixedFreeString
|
||||
|
||||
var lines []string
|
||||
|
||||
swapRoot := maybeArray || maybeString || prefix != ""
|
||||
|
||||
// write down the computed rules.
|
||||
// if maybeArray is true, we need to add the array rule and slightly tweak the root rule
|
||||
for name, rule := range rules {
|
||||
if swapRoot && name == "root" {
|
||||
name = "realvalue"
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("%s ::= %s", name, rule))
|
||||
}
|
||||
|
||||
if !swapRoot {
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
newRoot := "realvalue"
|
||||
if maybeArray {
|
||||
newRoot = "arr | realvalue"
|
||||
}
|
||||
|
||||
freestringRule := "mixedstring"
|
||||
if noMixedFreeString {
|
||||
freestringRule = "freestring"
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
// quote newlines in suffix
|
||||
prefix = utils.EscapeNewLines(prefix)
|
||||
|
||||
if maybeArray && maybeString {
|
||||
newRoot = "(" + newRoot + ")"
|
||||
}
|
||||
|
||||
if maybeString {
|
||||
//newRoot = "( (\"" + suffix + "\" " + newRoot + ") | freestring ) "
|
||||
newRoot = "( \"" + prefix + "\" " + newRoot + " | " + freestringRule + " ) "
|
||||
} else {
|
||||
newRoot = "\"" + prefix + "\" " + "" + newRoot + ""
|
||||
}
|
||||
} else if maybeString {
|
||||
if maybeArray {
|
||||
// newRoot = "(" + newRoot + ")"
|
||||
}
|
||||
|
||||
newRoot = freestringRule + " | " + newRoot
|
||||
}
|
||||
|
||||
lines = append(lines, fmt.Sprintf("%s ::= %s", "root", newRoot))
|
||||
if disableParallelNewLines {
|
||||
lines = append(lines, array)
|
||||
} else {
|
||||
lines = append(lines, arrayNewLines)
|
||||
}
|
||||
|
||||
if maybeArray {
|
||||
if grammarOpts.ExpectStringsAfterJSON {
|
||||
lines = append(lines, `mixedstring ::= freestring | freestring arr freestring | (freestring realvalue freestring)* | realvalue | arr`)
|
||||
} else {
|
||||
lines = append(lines, `mixedstring ::= freestring | freestring arr | freestring realvalue | realvalue | arr`)
|
||||
}
|
||||
} else {
|
||||
if grammarOpts.ExpectStringsAfterJSON {
|
||||
lines = append(lines, `mixedstring ::= freestring | (freestring realvalue freestring)* | realvalue`)
|
||||
} else {
|
||||
lines = append(lines, `mixedstring ::= freestring | freestring realvalue | realvalue`)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
33
pkg/functions/grammars/types.go
Normal file
33
pkg/functions/grammars/types.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package grammars
|
||||
|
||||
type SchemaConverterType int
|
||||
|
||||
const (
|
||||
JSONSchema SchemaConverterType = iota
|
||||
LLama31Schema
|
||||
)
|
||||
|
||||
const (
|
||||
LlamaType string = "llama3.1"
|
||||
JSONType string = "json"
|
||||
)
|
||||
|
||||
func (s SchemaConverterType) String() string {
|
||||
switch s {
|
||||
case JSONSchema:
|
||||
return JSONType
|
||||
case LLama31Schema:
|
||||
return LlamaType
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func NewType(t string) SchemaConverterType {
|
||||
switch t {
|
||||
case JSONType:
|
||||
return JSONSchema
|
||||
case LlamaType:
|
||||
return LLama31Schema
|
||||
}
|
||||
return JSONSchema
|
||||
}
|
||||
28
pkg/functions/json_mode.go
Normal file
28
pkg/functions/json_mode.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package functions
|
||||
|
||||
const (
|
||||
JSONBNF = `root ::= object
|
||||
value ::= object | array | string | number | ("true" | "false" | "null") ws
|
||||
|
||||
object ::=
|
||||
"{" ws (
|
||||
string ":" ws value
|
||||
("," ws string ":" ws value)*
|
||||
)? "}" ws
|
||||
|
||||
array ::=
|
||||
"[" ws (
|
||||
value
|
||||
("," ws value)*
|
||||
)? "]" ws
|
||||
|
||||
string ::=
|
||||
"\"" (
|
||||
[^"\\] |
|
||||
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) # escapes
|
||||
)* "\"" ws
|
||||
|
||||
number ::= ("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? ws
|
||||
|
||||
ws ::= ([ \t\n] ws)?`
|
||||
)
|
||||
@@ -2,9 +2,12 @@ package functions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAI/pkg/functions/grammars"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -20,7 +23,9 @@ type GrammarConfig struct {
|
||||
MixedMode bool `yaml:"mixed_mode"`
|
||||
|
||||
// NoMixedFreeString disables the mixed mode for free strings
|
||||
// In this way if the LLM selects a free string, it won't be mixed necessarly with JSON objects
|
||||
// In this way if the LLM selects a free string, it won't be mixed necessarly with JSON objects.
|
||||
// For example, if enabled the LLM or returns a JSON object or a free string, but not a mix of both
|
||||
// If disabled(default): the LLM can return a JSON object surrounded by free strings (e.g. `this is the JSON result: { "bar": "baz" } for your question`). This forces the LLM to return at least a JSON object, but its not going to be strict
|
||||
NoMixedFreeString bool `yaml:"no_mixed_free_string"`
|
||||
|
||||
// NoGrammar disables the grammar parsing and parses the responses directly from the LLM
|
||||
@@ -37,6 +42,10 @@ type GrammarConfig struct {
|
||||
// for instance name,arguments will make print { "name": "foo", "arguments": { "bar": "baz" } }
|
||||
// instead of { "arguments": { "bar": "baz" }, "name": "foo" }
|
||||
PropOrder string `yaml:"properties_order"`
|
||||
|
||||
// SchemaType can be configured to use a specific schema type to force the grammar
|
||||
// available : json, llama3.1
|
||||
SchemaType string `yaml:"schema_type"`
|
||||
}
|
||||
|
||||
// FunctionsConfig is the configuration for the tool/function call.
|
||||
@@ -76,7 +85,8 @@ type FunctionsConfig struct {
|
||||
// FunctionName enable the LLM to return { "name": "function_name", "arguments": { "arg1": "value1", "arg2": "value2" } }
|
||||
// instead of { "function": "function_name", "arguments": { "arg1": "value1", "arg2": "value2" } }.
|
||||
// This might be useful for certain models trained with the function name as the first token.
|
||||
FunctionName bool `yaml:"return_name_in_function_response"`
|
||||
FunctionNameKey string `yaml:"function_name_key"`
|
||||
FunctionArgumentsKey string `yaml:"function_arguments_key"`
|
||||
}
|
||||
|
||||
type ReplaceResult struct {
|
||||
@@ -89,28 +99,36 @@ type FuncCallResults struct {
|
||||
Arguments string
|
||||
}
|
||||
|
||||
func (g GrammarConfig) Options() []func(o *GrammarOption) {
|
||||
opts := []func(o *GrammarOption){}
|
||||
if g.MixedMode {
|
||||
opts = append(opts, EnableMaybeString)
|
||||
func (g FunctionsConfig) GrammarOptions() []func(o *grammars.GrammarOption) {
|
||||
opts := []func(o *grammars.GrammarOption){}
|
||||
if g.GrammarConfig.MixedMode {
|
||||
opts = append(opts, grammars.EnableMaybeString)
|
||||
}
|
||||
if g.ParallelCalls {
|
||||
opts = append(opts, EnableMaybeArray)
|
||||
if g.GrammarConfig.ParallelCalls {
|
||||
opts = append(opts, grammars.EnableMaybeArray)
|
||||
}
|
||||
if g.DisableParallelNewLines {
|
||||
opts = append(opts, DisableParallelNewLines)
|
||||
if g.GrammarConfig.DisableParallelNewLines {
|
||||
opts = append(opts, grammars.DisableParallelNewLines)
|
||||
}
|
||||
if g.Prefix != "" {
|
||||
opts = append(opts, SetPrefix(g.Prefix))
|
||||
if g.GrammarConfig.Prefix != "" {
|
||||
opts = append(opts, grammars.SetPrefix(g.GrammarConfig.Prefix))
|
||||
}
|
||||
if g.NoMixedFreeString {
|
||||
opts = append(opts, NoMixedFreeString)
|
||||
if g.GrammarConfig.NoMixedFreeString {
|
||||
opts = append(opts, grammars.NoMixedFreeString)
|
||||
}
|
||||
if g.ExpectStringsAfterJSON {
|
||||
opts = append(opts, ExpectStringsAfterJSON)
|
||||
if g.GrammarConfig.ExpectStringsAfterJSON {
|
||||
opts = append(opts, grammars.ExpectStringsAfterJSON)
|
||||
}
|
||||
|
||||
opts = append(opts, SetPropOrder(g.PropOrder))
|
||||
if g.GrammarConfig.SchemaType != "" {
|
||||
opts = append(opts, grammars.WithSchemaType(grammars.NewType(g.GrammarConfig.SchemaType)))
|
||||
}
|
||||
|
||||
if g.FunctionNameKey != "" {
|
||||
opts = append(opts, grammars.WithFunctionName(g.FunctionNameKey))
|
||||
}
|
||||
|
||||
opts = append(opts, grammars.SetPropOrder(g.GrammarConfig.PropOrder))
|
||||
return opts
|
||||
}
|
||||
|
||||
@@ -145,6 +163,47 @@ func ParseTextContent(llmresult string, functionConfig FunctionsConfig) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ParseJSON is a function that parses a JSON string that might contain multiple JSON objects
|
||||
// and syntax errors in between by shifting the offset
|
||||
// This for e.g. allow to parse
|
||||
// { "foo": "bar" } invalid { "baz": "qux" }
|
||||
// into
|
||||
// [ { "foo": "bar" }, { "baz": "qux" } ]
|
||||
// Credits to Michael Yang (https://github.com/mxyng) for the original implementation
|
||||
// This is a slighly reworked version, improved for readability and error handling
|
||||
func ParseJSON(s string) ([]map[string]any, error) {
|
||||
var objs []map[string]any
|
||||
offset := 0
|
||||
|
||||
for offset < len(s) {
|
||||
var obj map[string]any
|
||||
decoder := json.NewDecoder(strings.NewReader(s[offset:]))
|
||||
|
||||
err := decoder.Decode(&obj)
|
||||
switch {
|
||||
case errors.Is(err, io.EOF):
|
||||
return objs, nil
|
||||
case err == nil:
|
||||
offset += int(decoder.InputOffset())
|
||||
objs = append(objs, obj)
|
||||
default: // handle the error type
|
||||
var syntaxErr *json.SyntaxError
|
||||
var unmarshalTypeErr *json.UnmarshalTypeError
|
||||
|
||||
switch {
|
||||
case errors.As(err, &syntaxErr):
|
||||
offset += int(syntaxErr.Offset)
|
||||
case errors.As(err, &unmarshalTypeErr):
|
||||
offset += int(unmarshalTypeErr.Offset)
|
||||
default:
|
||||
return objs, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncCallResults {
|
||||
|
||||
log.Debug().Msgf("LLM result: %s", llmresult)
|
||||
@@ -157,9 +216,13 @@ func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncC
|
||||
}
|
||||
log.Debug().Msgf("LLM result(function cleanup): %s", llmresult)
|
||||
|
||||
functionNameKey := "function"
|
||||
if functionConfig.FunctionName {
|
||||
functionNameKey = "name"
|
||||
functionNameKey := defaultFunctionNameKey
|
||||
functionArgumentsKey := defaultFunctionArgumentsKey
|
||||
if functionConfig.FunctionNameKey != "" {
|
||||
functionNameKey = functionConfig.FunctionNameKey
|
||||
}
|
||||
if functionConfig.FunctionArgumentsKey != "" {
|
||||
functionArgumentsKey = functionConfig.FunctionArgumentsKey
|
||||
}
|
||||
|
||||
results := []FuncCallResults{}
|
||||
@@ -170,19 +233,13 @@ func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncC
|
||||
result = make([]FuncCallResults, 0)
|
||||
|
||||
for _, s := range results {
|
||||
var ss []map[string]interface{}
|
||||
var ss []map[string]any
|
||||
|
||||
s = utils.EscapeNewLines(s)
|
||||
err := json.Unmarshal([]byte(s), &ss)
|
||||
ss, err := ParseJSON(s)
|
||||
//err := json.Unmarshal([]byte(s), &ss)
|
||||
if err != nil {
|
||||
// If the LLM result is a single object, try unmarshaling it into a single map
|
||||
var singleObj map[string]interface{}
|
||||
err = json.Unmarshal([]byte(s), &singleObj)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Str("escapedLLMResult", s).Msg("unable to unmarshal llm result in a single object or an array of JSON objects")
|
||||
} else {
|
||||
ss = []map[string]interface{}{singleObj}
|
||||
}
|
||||
log.Debug().Err(err).Str("escapedLLMResult", s).Msg("unable to unmarshal llm result in a single object or an array of JSON objects")
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Function return: %s %+v", s, ss)
|
||||
@@ -195,7 +252,7 @@ func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncC
|
||||
//return result, fmt.Errorf("unable to find function name in result")
|
||||
}
|
||||
// Similarly, while here arguments is a map[string]interface{}, OpenAI actually want a stringified object
|
||||
args, ok := s["arguments"] // arguments needs to be a string, but we return an object from the grammar result (TODO: fix)
|
||||
args, ok := s[functionArgumentsKey] // arguments needs to be a string, but we return an object from the grammar result (TODO: fix)
|
||||
if !ok {
|
||||
continue
|
||||
//return result, fmt.Errorf("unable to find arguments in result")
|
||||
@@ -253,7 +310,7 @@ func ParseFunctionCall(llmresult string, functionConfig FunctionsConfig) []FuncC
|
||||
if functionName == "" {
|
||||
return results
|
||||
}
|
||||
results = append(results, FuncCallResults{Name: result[functionNameKey], Arguments: result["arguments"]})
|
||||
results = append(results, FuncCallResults{Name: result[functionNameKey], Arguments: result[functionArgumentsKey]})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -16,7 +16,7 @@ var _ = Describe("LocalAI function parse tests", func() {
|
||||
|
||||
Context("when using grammars and single result expected", func() {
|
||||
It("should parse the function name and arguments correctly", func() {
|
||||
input := `{"function": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
input := `{"name": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(1))
|
||||
@@ -28,13 +28,22 @@ var _ = Describe("LocalAI function parse tests", func() {
|
||||
Context("when not using grammars and regex is needed", func() {
|
||||
It("should extract function name and arguments from the regex", func() {
|
||||
input := `add({"x":5,"y":3})`
|
||||
functionConfig.ResponseRegex = []string{`(?P<function>\w+)\s*\((?P<arguments>.*)\)`}
|
||||
functionConfig.ResponseRegex = []string{`(?P<name>\w+)\s*\((?P<arguments>.*)\)`}
|
||||
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0].Name).To(Equal("add"))
|
||||
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`))
|
||||
})
|
||||
It("should extract function name and arguments from the regex", func() {
|
||||
input := `add({"x":5,"y":3})`
|
||||
functionConfig.ResponseRegex = []string{`(?P<function>\w+)\s*\((?P<arguments>.*)\)`}
|
||||
functionConfig.FunctionNameKey = "function"
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(1))
|
||||
Expect(results[0].Name).To(Equal("add"))
|
||||
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when having invalid input", func() {
|
||||
@@ -53,7 +62,7 @@ var _ = Describe("LocalAI function parse tests", func() {
|
||||
|
||||
Context("when parallel calls are enabled", func() {
|
||||
It("should handle multiple function calls", func() {
|
||||
input := `[{"function": "add", "arguments": {"x": 5, "y": 3}}, {"function": "subtract", "arguments": {"x": 10, "y": 7}}]`
|
||||
input := `[{"name": "add", "arguments": {"x": 5, "y": 3}}, {"name": "subtract", "arguments": {"x": 10, "y": 7}}]`
|
||||
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(2))
|
||||
@@ -66,8 +75,8 @@ var _ = Describe("LocalAI function parse tests", func() {
|
||||
|
||||
Context("without grammars and without regex", func() {
|
||||
It("should parse the function name and arguments correctly with the name key", func() {
|
||||
input := `{"name": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
functionConfig.FunctionName = true
|
||||
input := `{"function": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
functionConfig.FunctionNameKey = "function"
|
||||
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(1))
|
||||
@@ -76,7 +85,7 @@ var _ = Describe("LocalAI function parse tests", func() {
|
||||
})
|
||||
|
||||
It("should parse the function name and arguments correctly with the function key", func() {
|
||||
input := `{"function": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
input := `{"name": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(1))
|
||||
@@ -87,7 +96,7 @@ var _ = Describe("LocalAI function parse tests", func() {
|
||||
It("should parse the result by matching the JSONRegexMatch", func() {
|
||||
input := `
|
||||
<tool_call>
|
||||
{"function": "add", "arguments": {"x": 5, "y": 3}}
|
||||
{"name": "add", "arguments": {"x": 5, "y": 3}}
|
||||
</tool_call>`
|
||||
|
||||
functionConfig.JSONRegexMatch = []string{`(?s)<tool_call>(.*?)</tool_call>`}
|
||||
@@ -100,7 +109,7 @@ var _ = Describe("LocalAI function parse tests", func() {
|
||||
|
||||
It("should parse the result by matching the JSONRegexMatch", func() {
|
||||
input := `
|
||||
{"function": "add", "arguments": {"x": 5, "y": 3}}
|
||||
{"name": "add", "arguments": {"x": 5, "y": 3}}
|
||||
</tool_call>`
|
||||
|
||||
functionConfig.JSONRegexMatch = []string{`(?s)(.*?)</tool_call>`}
|
||||
@@ -110,13 +119,21 @@ var _ = Describe("LocalAI function parse tests", func() {
|
||||
Expect(results[0].Name).To(Equal("add"))
|
||||
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`))
|
||||
})
|
||||
|
||||
It("should parse the result even with invalid JSON", func() {
|
||||
input := `{"name": "add", "arguments": {"x": 5, "y": 3}} invalid {"name": "add", "arguments": {"x": 5, "y": 3}}`
|
||||
results := ParseFunctionCall(input, functionConfig)
|
||||
Expect(results).To(HaveLen(2))
|
||||
Expect(results[0].Name).To(Equal("add"))
|
||||
Expect(results[0].Arguments).To(Equal(`{"x":5,"y":3}`))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when using ReplaceResults to clean up input", func() {
|
||||
It("should replace text before and after JSON blob", func() {
|
||||
input := `
|
||||
Some text before the JSON
|
||||
{"function": "add", "arguments": {"x": 5, "y": 3}}
|
||||
{"name": "add", "arguments": {"x": 5, "y": 3}}
|
||||
Some text after the JSON
|
||||
`
|
||||
|
||||
@@ -134,7 +151,7 @@ Some text after the JSON
|
||||
It("should replace text before and after array JSON blob", func() {
|
||||
input := `
|
||||
Some text before the JSON
|
||||
[{"function": "add", "arguments": {"x": 5, "y": 3}}, {"function": "subtract", "arguments": {"x": 10, "y": 7}}]
|
||||
[{"name": "add", "arguments": {"x": 5, "y": 3}}, {"name": "subtract", "arguments": {"x": 10, "y": 7}}]
|
||||
Some text after the JSON
|
||||
`
|
||||
functionConfig.ReplaceFunctionResults = []ReplaceResult{
|
||||
@@ -153,7 +170,7 @@ Some text after the JSON
|
||||
It("should convert single-quoted key-value pairs to double-quoted and escape double quotes within values", func() {
|
||||
input := `
|
||||
Some text before the JSON
|
||||
{'function': '"add"', 'arguments': {'x': 5, 'z': '"v"', 'y': 'v"value"'}}
|
||||
{'name': '"add"', 'arguments': {'x': 5, 'z': '"v"', 'y': 'v"value"'}}
|
||||
Some text after the JSON
|
||||
`
|
||||
functionConfig.JSONRegexMatch = []string{`(?s)<tool_call>(.*?)</tool_call>`}
|
||||
@@ -186,7 +203,7 @@ Some text after the JSON
|
||||
It("should convert single-quoted key-value pairs to double-quoted and escape double quotes within values", func() {
|
||||
input := `
|
||||
Some text before the JSON
|
||||
<tool_call>{'function': '"add"', 'arguments': {'x': 5, 'z': '"v"', 'y': 'v"value"'}}</tool_call>
|
||||
<tool_call>{'name': '"add"', 'arguments': {'x': 5, 'z': '"v"', 'y': 'v"value"'}}</tool_call>
|
||||
Some text after the JSON
|
||||
`
|
||||
functionConfig.JSONRegexMatch = []string{`(?s)<tool_call>(.*?)</tool_call>`}
|
||||
@@ -219,8 +236,8 @@ Some text after the JSON
|
||||
It("should detect multiple functions call where the JSONRegexMatch is repeated", func() {
|
||||
input := `
|
||||
Some text before the JSON
|
||||
<tool_call>{"function": "add", "arguments": {"x": 5, "y": 3}}</tool_call>
|
||||
<tool_call>{"function": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
<tool_call>{"name": "add", "arguments": {"x": 5, "y": 3}}</tool_call>
|
||||
<tool_call>{"name": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
Some text after the JSON
|
||||
`
|
||||
functionConfig.JSONRegexMatch = []string{`(?s)<tool_call>(.*?)</tool_call>`}
|
||||
@@ -240,7 +257,7 @@ Some text after the JSON
|
||||
<sketchpad>
|
||||
roses are red
|
||||
</sketchpad>
|
||||
<tool_call>{"function": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
<tool_call>{"name": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
Some text after the JSON
|
||||
`
|
||||
functionConfig.CaptureLLMResult = []string{`(?s)<sketchpad>(.*?)</sketchpad>`}
|
||||
@@ -251,7 +268,7 @@ roses are red
|
||||
It("Defaults to empty if doesn't catch any", func() {
|
||||
input := `
|
||||
Some text before the JSON
|
||||
<tool_call>{"function": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
<tool_call>{"name": "subtract", "arguments": {"x": 10, "y": 7}}</tool_call>
|
||||
Some text after the JSON
|
||||
`
|
||||
functionConfig.CaptureLLMResult = []string{`(?s)<sketchpad>(.*?)</sketchpad>`}
|
||||
@@ -259,4 +276,74 @@ roses are red
|
||||
Expect(results).To(Equal(""))
|
||||
})
|
||||
})
|
||||
Context("ParseJSON - when given valid JSON strings", func() {
|
||||
It("should parse multiple JSON objects", func() {
|
||||
input := `{"key1": "value1"} {"key2": "value2"}`
|
||||
expected := []map[string]any{
|
||||
{"key1": "value1"},
|
||||
{"key2": "value2"},
|
||||
}
|
||||
result, err := ParseJSON(input)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(expected))
|
||||
})
|
||||
|
||||
It("should parse a single JSON object with various types", func() {
|
||||
input := `{"key1": "value1", "key2": 2}`
|
||||
expected := []map[string]any{
|
||||
{"key1": "value1", "key2": float64(2)},
|
||||
}
|
||||
result, err := ParseJSON(input)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(expected))
|
||||
})
|
||||
It("should handle JSON without syntax errors gracefully", func() {
|
||||
input := `{"key1": "value1"}`
|
||||
expected := []map[string]any{
|
||||
{"key1": "value1"},
|
||||
}
|
||||
result, err := ParseJSON(input)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(expected))
|
||||
})
|
||||
It("should handle JSON without syntax errors gracefully", func() {
|
||||
input := `[{"key1": "value1"}]`
|
||||
expected := []map[string]any{
|
||||
{"key1": "value1"},
|
||||
}
|
||||
result, err := ParseJSON(input)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
|
||||
Context("ParseJSON - when given invalid JSON strings", func() {
|
||||
It("should return an error for completely invalid JSON", func() {
|
||||
input := `invalid json`
|
||||
result, err := ParseJSON(input)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(result).To(BeNil())
|
||||
})
|
||||
|
||||
It("should skip invalid JSON parts and parse valid parts", func() {
|
||||
input := `{"key1": "value1"} invalid {"key2": "value2"}`
|
||||
expected := []map[string]any{
|
||||
{"key1": "value1"},
|
||||
{"key2": "value2"},
|
||||
}
|
||||
result, err := ParseJSON(input)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(expected))
|
||||
})
|
||||
|
||||
PIt("should handle JSON with syntax errors gracefully", func() {
|
||||
input := `{"key1": "value1", "key2": }`
|
||||
expected := []map[string]any{
|
||||
{"key1": "value1"},
|
||||
}
|
||||
result, err := ParseJSON(input)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -212,7 +212,7 @@ func selectGRPCProcess(backend, assetDir string, f16 bool) string {
|
||||
grpcProcess = p
|
||||
foundCUDA = true
|
||||
} else {
|
||||
log.Info().Msgf("GPU device found but no CUDA backend present")
|
||||
log.Debug().Msgf("Nvidia GPU device found, no embedded CUDA variant found. You can ignore this message if you are using container with CUDA support")
|
||||
}
|
||||
}
|
||||
if strings.Contains(gpu.String(), "amd") {
|
||||
@@ -222,7 +222,7 @@ func selectGRPCProcess(backend, assetDir string, f16 bool) string {
|
||||
grpcProcess = p
|
||||
foundAMDGPU = true
|
||||
} else {
|
||||
log.Info().Msgf("GPU device found but no HIPBLAS backend present")
|
||||
log.Debug().Msgf("AMD GPU device found, no embedded HIPBLAS variant found. You can ignore this message if you are using container with HIPBLAS support")
|
||||
}
|
||||
}
|
||||
if strings.Contains(gpu.String(), "intel") {
|
||||
@@ -236,7 +236,7 @@ func selectGRPCProcess(backend, assetDir string, f16 bool) string {
|
||||
grpcProcess = p
|
||||
foundIntelGPU = true
|
||||
} else {
|
||||
log.Info().Msgf("GPU device found but no Intel backend present")
|
||||
log.Debug().Msgf("Intel GPU device found, no embedded SYCL variant found. You can ignore this message if you are using container with SYCL support")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@ var knownModelsNameSuffixToSkip []string = []string{
|
||||
".yaml",
|
||||
".yml",
|
||||
".json",
|
||||
".txt",
|
||||
".md",
|
||||
".MD",
|
||||
".DS_Store",
|
||||
".",
|
||||
".partial",
|
||||
|
||||
@@ -18,3 +18,15 @@ func RandString(n int) string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func Unique(arr []string) []string {
|
||||
unique := make(map[string]bool)
|
||||
var result []string
|
||||
for _, item := range arr {
|
||||
if _, ok := unique[item]; !ok {
|
||||
unique[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -700,18 +700,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.Argument": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.Function": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -727,48 +715,19 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.FunctionName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"const": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.FunctionProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"arguments": {
|
||||
"$ref": "#/definitions/functions.Argument"
|
||||
},
|
||||
"function": {
|
||||
"$ref": "#/definitions/functions.FunctionName"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.ItemFunction": {
|
||||
"functions.Item": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"$ref": "#/definitions/functions.FunctionProperties"
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.ItemName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"$ref": "#/definitions/functions.NameProperties"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.JSONFunctionStructureFunction": {
|
||||
"functions.JSONFunctionStructure": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$defs": {
|
||||
@@ -778,49 +737,17 @@ const docTemplate = `{
|
||||
"anyOf": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/functions.ItemFunction"
|
||||
"$ref": "#/definitions/functions.Item"
|
||||
}
|
||||
},
|
||||
"oneOf": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/functions.ItemFunction"
|
||||
"$ref": "#/definitions/functions.Item"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.JSONFunctionStructureName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$defs": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"anyOf": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/functions.ItemName"
|
||||
}
|
||||
},
|
||||
"oneOf": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/functions.ItemName"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.NameProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"arguments": {
|
||||
"$ref": "#/definitions/functions.Argument"
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/definitions/functions.FunctionName"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.Tool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1488,10 +1415,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"grammar_json_functions": {
|
||||
"$ref": "#/definitions/functions.JSONFunctionStructureFunction"
|
||||
},
|
||||
"grammar_json_name": {
|
||||
"$ref": "#/definitions/functions.JSONFunctionStructureName"
|
||||
"$ref": "#/definitions/functions.JSONFunctionStructure"
|
||||
},
|
||||
"ignore_eos": {
|
||||
"type": "boolean"
|
||||
|
||||
@@ -693,18 +693,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.Argument": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.Function": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -720,48 +708,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.FunctionName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"const": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.FunctionProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"arguments": {
|
||||
"$ref": "#/definitions/functions.Argument"
|
||||
},
|
||||
"function": {
|
||||
"$ref": "#/definitions/functions.FunctionName"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.ItemFunction": {
|
||||
"functions.Item": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"$ref": "#/definitions/functions.FunctionProperties"
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.ItemName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"$ref": "#/definitions/functions.NameProperties"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.JSONFunctionStructureFunction": {
|
||||
"functions.JSONFunctionStructure": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$defs": {
|
||||
@@ -771,49 +730,17 @@
|
||||
"anyOf": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/functions.ItemFunction"
|
||||
"$ref": "#/definitions/functions.Item"
|
||||
}
|
||||
},
|
||||
"oneOf": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/functions.ItemFunction"
|
||||
"$ref": "#/definitions/functions.Item"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.JSONFunctionStructureName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$defs": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"anyOf": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/functions.ItemName"
|
||||
}
|
||||
},
|
||||
"oneOf": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/functions.ItemName"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.NameProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"arguments": {
|
||||
"$ref": "#/definitions/functions.Argument"
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/definitions/functions.FunctionName"
|
||||
}
|
||||
}
|
||||
},
|
||||
"functions.Tool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1481,10 +1408,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"grammar_json_functions": {
|
||||
"$ref": "#/definitions/functions.JSONFunctionStructureFunction"
|
||||
},
|
||||
"grammar_json_name": {
|
||||
"$ref": "#/definitions/functions.JSONFunctionStructureName"
|
||||
"$ref": "#/definitions/functions.JSONFunctionStructure"
|
||||
},
|
||||
"ignore_eos": {
|
||||
"type": "boolean"
|
||||
|
||||
@@ -7,14 +7,6 @@ definitions:
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
functions.Argument:
|
||||
properties:
|
||||
properties:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
functions.Function:
|
||||
properties:
|
||||
description:
|
||||
@@ -25,67 +17,28 @@ definitions:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
type: object
|
||||
functions.FunctionName:
|
||||
properties:
|
||||
const:
|
||||
type: string
|
||||
type: object
|
||||
functions.FunctionProperties:
|
||||
properties:
|
||||
arguments:
|
||||
$ref: '#/definitions/functions.Argument'
|
||||
function:
|
||||
$ref: '#/definitions/functions.FunctionName'
|
||||
type: object
|
||||
functions.ItemFunction:
|
||||
functions.Item:
|
||||
properties:
|
||||
properties:
|
||||
$ref: '#/definitions/functions.FunctionProperties'
|
||||
additionalProperties: true
|
||||
type: object
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
functions.ItemName:
|
||||
properties:
|
||||
properties:
|
||||
$ref: '#/definitions/functions.NameProperties'
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
functions.JSONFunctionStructureFunction:
|
||||
functions.JSONFunctionStructure:
|
||||
properties:
|
||||
$defs:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
anyOf:
|
||||
items:
|
||||
$ref: '#/definitions/functions.ItemFunction'
|
||||
$ref: '#/definitions/functions.Item'
|
||||
type: array
|
||||
oneOf:
|
||||
items:
|
||||
$ref: '#/definitions/functions.ItemFunction'
|
||||
$ref: '#/definitions/functions.Item'
|
||||
type: array
|
||||
type: object
|
||||
functions.JSONFunctionStructureName:
|
||||
properties:
|
||||
$defs:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
anyOf:
|
||||
items:
|
||||
$ref: '#/definitions/functions.ItemName'
|
||||
type: array
|
||||
oneOf:
|
||||
items:
|
||||
$ref: '#/definitions/functions.ItemName'
|
||||
type: array
|
||||
type: object
|
||||
functions.NameProperties:
|
||||
properties:
|
||||
arguments:
|
||||
$ref: '#/definitions/functions.Argument'
|
||||
name:
|
||||
$ref: '#/definitions/functions.FunctionName'
|
||||
type: object
|
||||
functions.Tool:
|
||||
properties:
|
||||
function:
|
||||
@@ -538,9 +491,7 @@ definitions:
|
||||
description: A grammar to constrain the LLM output
|
||||
type: string
|
||||
grammar_json_functions:
|
||||
$ref: '#/definitions/functions.JSONFunctionStructureFunction'
|
||||
grammar_json_name:
|
||||
$ref: '#/definitions/functions.JSONFunctionStructureName'
|
||||
$ref: '#/definitions/functions.JSONFunctionStructure'
|
||||
ignore_eos:
|
||||
type: boolean
|
||||
input: {}
|
||||
|
||||
Reference in New Issue
Block a user