-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(p2p): add network explorer and community pools (#3125)
* WIP Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Wire up a simple explorer DB Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * wip Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * WIP Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactor: group services id so can be identified easily in the ledger table Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(discovery): discovery service now gather worker informations correctly Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(explorer): display network token Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(explorer): display form to add new networks Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(explorer): stop from overwriting networks Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(explorer): display only networks with active workers Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(explorer): list only clusters in a network if it has online workers Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * remove invalid and inactive networks if networks have no workers delete them from the database, similarly, if invalid. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * ci: add workflow to deploy new explorer versions automatically Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * build-api: build with p2p tag Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Allow to specify a connection timeout Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * logging Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Better p2p defaults Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Set loglevel Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Fix dht enable Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Default to info for loglevel Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Add navbar Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Slightly improve rendering Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Allow to copy the token easily Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * ci fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
- Loading branch information
Showing
19 changed files
with
1,082 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
name: Explorer deployment | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
tags: | ||
- 'v*' | ||
|
||
concurrency: | ||
group: ci-deploy-${{ github.head_ref || github.ref }}-${{ github.repository }} | ||
|
||
jobs: | ||
build-linux: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Clone | ||
uses: actions/checkout@v4 | ||
with: | ||
submodules: true | ||
- uses: actions/setup-go@v5 | ||
with: | ||
go-version: '1.21.x' | ||
cache: false | ||
- name: Dependencies | ||
run: | | ||
sudo apt-get update | ||
sudo apt-get install -y wget curl build-essential ffmpeg protobuf-compiler ccache upx-ucl gawk cmake libgmock-dev | ||
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 | ||
make protogen-go | ||
- name: Build api | ||
run: | | ||
make build-api | ||
- name: rm | ||
uses: appleboy/ssh-action@v1.0.3 | ||
with: | ||
host: ${{ secrets.EXPLORER_SSH_HOST }} | ||
username: ${{ secrets.EXPLORER_SSH_USERNAME }} | ||
key: ${{ secrets.EXPLORER_SSH_KEY }} | ||
port: ${{ secrets.EXPLORER_SSH_PORT }} | ||
script: | | ||
sudo rm -rf local-ai/ || true | ||
- name: copy file via ssh | ||
uses: appleboy/scp-action@v0.1.7 | ||
with: | ||
host: ${{ secrets.EXPLORER_SSH_HOST }} | ||
username: ${{ secrets.EXPLORER_SSH_USERNAME }} | ||
key: ${{ secrets.EXPLORER_SSH_KEY }} | ||
port: ${{ secrets.EXPLORER_SSH_PORT }} | ||
source: "local-ai" | ||
overwrite: true | ||
rm: true | ||
target: ./local-ai | ||
- name: restarting | ||
uses: appleboy/ssh-action@v1.0.3 | ||
with: | ||
host: ${{ secrets.EXPLORER_SSH_HOST }} | ||
username: ${{ secrets.EXPLORER_SSH_USERNAME }} | ||
key: ${{ secrets.EXPLORER_SSH_KEY }} | ||
port: ${{ secrets.EXPLORER_SSH_PORT }} | ||
script: | | ||
sudo cp -rfv local-ai/local-ai /usr/bin/local-ai | ||
sudo systemctl restart local-ai |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package cli | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
cliContext "github.com/mudler/LocalAI/core/cli/context" | ||
"github.com/mudler/LocalAI/core/explorer" | ||
"github.com/mudler/LocalAI/core/http" | ||
) | ||
|
||
type ExplorerCMD struct { | ||
Address string `env:"LOCALAI_ADDRESS,ADDRESS" default:":8080" help:"Bind address for the API server" group:"api"` | ||
PoolDatabase string `env:"LOCALAI_POOL_DATABASE,POOL_DATABASE" default:"explorer.json" help:"Path to the pool database" group:"api"` | ||
ConnectionTimeout string `env:"LOCALAI_CONNECTION_TIMEOUT,CONNECTION_TIMEOUT" default:"2m" help:"Connection timeout for the explorer" group:"api"` | ||
} | ||
|
||
func (e *ExplorerCMD) Run(ctx *cliContext.Context) error { | ||
|
||
db, err := explorer.NewDatabase(e.PoolDatabase) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
dur, err := time.ParseDuration(e.ConnectionTimeout) | ||
if err != nil { | ||
return err | ||
} | ||
ds := explorer.NewDiscoveryServer(db, dur) | ||
|
||
go ds.Start(context.Background()) | ||
appHTTP := http.Explorer(db, ds) | ||
|
||
return appHTTP.Listen(e.Address) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package explorer | ||
|
||
// A simple JSON database for storing and retrieving p2p network tokens and a name and description. | ||
|
||
import ( | ||
"encoding/json" | ||
"os" | ||
"sort" | ||
"sync" | ||
) | ||
|
||
// Database is a simple JSON database for storing and retrieving p2p network tokens and a name and description. | ||
type Database struct { | ||
sync.RWMutex | ||
path string | ||
data map[string]TokenData | ||
} | ||
|
||
// TokenData is a p2p network token with a name and description. | ||
type TokenData struct { | ||
Name string `json:"name"` | ||
Description string `json:"description"` | ||
} | ||
|
||
// NewDatabase creates a new Database with the given path. | ||
func NewDatabase(path string) (*Database, error) { | ||
db := &Database{ | ||
data: make(map[string]TokenData), | ||
path: path, | ||
} | ||
return db, db.load() | ||
} | ||
|
||
// Get retrieves a Token from the Database by its token. | ||
func (db *Database) Get(token string) (TokenData, bool) { | ||
db.RLock() | ||
defer db.RUnlock() | ||
t, ok := db.data[token] | ||
return t, ok | ||
} | ||
|
||
// Set stores a Token in the Database by its token. | ||
func (db *Database) Set(token string, t TokenData) error { | ||
db.Lock() | ||
db.data[token] = t | ||
db.Unlock() | ||
|
||
return db.Save() | ||
} | ||
|
||
// Delete removes a Token from the Database by its token. | ||
func (db *Database) Delete(token string) error { | ||
db.Lock() | ||
delete(db.data, token) | ||
db.Unlock() | ||
return db.Save() | ||
} | ||
|
||
func (db *Database) TokenList() []string { | ||
db.RLock() | ||
defer db.RUnlock() | ||
tokens := []string{} | ||
for k := range db.data { | ||
tokens = append(tokens, k) | ||
} | ||
|
||
sort.Slice(tokens, func(i, j int) bool { | ||
// sort by token | ||
return tokens[i] < tokens[j] | ||
}) | ||
|
||
return tokens | ||
} | ||
|
||
// load reads the Database from disk. | ||
func (db *Database) load() error { | ||
db.Lock() | ||
defer db.Unlock() | ||
|
||
if _, err := os.Stat(db.path); os.IsNotExist(err) { | ||
return nil | ||
} | ||
|
||
// Read the file from disk | ||
// Unmarshal the JSON into db.data | ||
f, err := os.ReadFile(db.path) | ||
if err != nil { | ||
return err | ||
} | ||
return json.Unmarshal(f, &db.data) | ||
} | ||
|
||
// Save writes the Database to disk. | ||
func (db *Database) Save() error { | ||
db.RLock() | ||
defer db.RUnlock() | ||
|
||
// Marshal db.data into JSON | ||
// Write the JSON to the file | ||
f, err := os.Create(db.path) | ||
if err != nil { | ||
return err | ||
} | ||
defer f.Close() | ||
return json.NewEncoder(f).Encode(db.data) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package explorer_test | ||
|
||
import ( | ||
"os" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
|
||
"github.com/mudler/LocalAI/core/explorer" | ||
) | ||
|
||
var _ = Describe("Database", func() { | ||
var ( | ||
dbPath string | ||
db *explorer.Database | ||
err error | ||
) | ||
|
||
BeforeEach(func() { | ||
// Create a temporary file path for the database | ||
dbPath = "test_db.json" | ||
db, err = explorer.NewDatabase(dbPath) | ||
Expect(err).To(BeNil()) | ||
}) | ||
|
||
AfterEach(func() { | ||
// Clean up the temporary database file | ||
os.Remove(dbPath) | ||
}) | ||
|
||
Context("when managing tokens", func() { | ||
It("should add and retrieve a token", func() { | ||
token := "token123" | ||
t := explorer.TokenData{Name: "TokenName", Description: "A test token"} | ||
|
||
err = db.Set(token, t) | ||
Expect(err).To(BeNil()) | ||
|
||
retrievedToken, exists := db.Get(token) | ||
Expect(exists).To(BeTrue()) | ||
Expect(retrievedToken).To(Equal(t)) | ||
}) | ||
|
||
It("should delete a token", func() { | ||
token := "token123" | ||
t := explorer.TokenData{Name: "TokenName", Description: "A test token"} | ||
|
||
err = db.Set(token, t) | ||
Expect(err).To(BeNil()) | ||
|
||
err = db.Delete(token) | ||
Expect(err).To(BeNil()) | ||
|
||
_, exists := db.Get(token) | ||
Expect(exists).To(BeFalse()) | ||
}) | ||
|
||
It("should persist data to disk", func() { | ||
token := "token123" | ||
t := explorer.TokenData{Name: "TokenName", Description: "A test token"} | ||
|
||
err = db.Set(token, t) | ||
Expect(err).To(BeNil()) | ||
|
||
// Recreate the database object to simulate reloading from disk | ||
db, err = explorer.NewDatabase(dbPath) | ||
Expect(err).To(BeNil()) | ||
|
||
retrievedToken, exists := db.Get(token) | ||
Expect(exists).To(BeTrue()) | ||
Expect(retrievedToken).To(Equal(t)) | ||
|
||
// Check the token list | ||
tokenList := db.TokenList() | ||
Expect(tokenList).To(ContainElement(token)) | ||
}) | ||
}) | ||
|
||
Context("when loading an empty or non-existent file", func() { | ||
It("should start with an empty database", func() { | ||
dbPath = "empty_db.json" | ||
db, err = explorer.NewDatabase(dbPath) | ||
Expect(err).To(BeNil()) | ||
|
||
_, exists := db.Get("nonexistent") | ||
Expect(exists).To(BeFalse()) | ||
|
||
// Clean up | ||
os.Remove(dbPath) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.