-
Notifications
You must be signed in to change notification settings - Fork 243
feat: allow to manage external localrecall instances #425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package webui | ||
|
|
||
| import ( | ||
| "io" | ||
| "time" | ||
| ) | ||
|
|
||
| // CollectionSearchResult is a single search result (content + metadata) for API responses. | ||
| type CollectionSearchResult struct { | ||
| Content string `json:"content"` | ||
| Metadata map[string]string `json:"metadata,omitempty"` | ||
| ID string `json:"id,omitempty"` | ||
| Similarity float32 `json:"similarity,omitempty"` | ||
| } | ||
|
|
||
| // CollectionSourceInfo is a single external source for a collection. | ||
| type CollectionSourceInfo struct { | ||
| URL string `json:"url"` | ||
| UpdateInterval int `json:"update_interval"` // minutes | ||
| LastUpdate time.Time `json:"last_update"` | ||
| } | ||
|
|
||
| // CollectionsBackend is the interface used by REST handlers for collection operations. | ||
| // It is implemented by in-process state (embedded) or by an HTTP client (when LocalRAG URL is set). | ||
| type CollectionsBackend interface { | ||
| ListCollections() ([]string, error) | ||
| CreateCollection(name string) error | ||
| Upload(collection, filename string, fileBody io.Reader) error | ||
| ListEntries(collection string) ([]string, error) | ||
| GetEntryContent(collection, entry string) (content string, chunkCount int, err error) | ||
| Search(collection, query string, maxResults int) ([]CollectionSearchResult, error) | ||
| Reset(collection string) error | ||
| DeleteEntry(collection, entry string) (remainingEntries []string, err error) | ||
| AddSource(collection, url string, intervalMin int) error | ||
| RemoveSource(collection, url string) error | ||
| ListSources(collection string) ([]CollectionSourceInfo, error) | ||
| // EntryExists is used by upload handler to avoid duplicate entries. | ||
| EntryExists(collection, entry string) bool | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,128 @@ | ||||||||||||
| package webui | ||||||||||||
|
|
||||||||||||
| import ( | ||||||||||||
| "io" | ||||||||||||
| "os" | ||||||||||||
| "path/filepath" | ||||||||||||
| "time" | ||||||||||||
|
|
||||||||||||
| "github.com/mudler/LocalAGI/pkg/localrag" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| // collectionsBackendHTTP implements CollectionsBackend using the LocalRAG HTTP API. | ||||||||||||
| type collectionsBackendHTTP struct { | ||||||||||||
| client *localrag.Client | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| var _ CollectionsBackend = (*collectionsBackendHTTP)(nil) | ||||||||||||
|
|
||||||||||||
| // NewCollectionsBackendHTTP returns a CollectionsBackend that delegates to the given HTTP client. | ||||||||||||
| func NewCollectionsBackendHTTP(client *localrag.Client) CollectionsBackend { | ||||||||||||
| return &collectionsBackendHTTP{client: client} | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) ListCollections() ([]string, error) { | ||||||||||||
| return b.client.ListCollections() | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) CreateCollection(name string) error { | ||||||||||||
| return b.client.CreateCollection(name) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) Upload(collection, filename string, fileBody io.Reader) error { | ||||||||||||
| tmpDir, err := os.MkdirTemp("", "localagi-upload") | ||||||||||||
| if err != nil { | ||||||||||||
| return err | ||||||||||||
| } | ||||||||||||
| defer os.RemoveAll(tmpDir) | ||||||||||||
| tmpPath := filepath.Join(tmpDir, filename) | ||||||||||||
| out, err := os.Create(tmpPath) | ||||||||||||
| if err != nil { | ||||||||||||
| return err | ||||||||||||
| } | ||||||||||||
| if _, err := io.Copy(out, fileBody); err != nil { | ||||||||||||
| out.Close() | ||||||||||||
| return err | ||||||||||||
| } | ||||||||||||
| if err := out.Close(); err != nil { | ||||||||||||
| return err | ||||||||||||
| } | ||||||||||||
| return b.client.Store(collection, tmpPath) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) ListEntries(collection string) ([]string, error) { | ||||||||||||
| return b.client.ListEntries(collection) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) GetEntryContent(collection, entry string) (string, int, error) { | ||||||||||||
| return b.client.GetEntryContent(collection, entry) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) Search(collection, query string, maxResults int) ([]CollectionSearchResult, error) { | ||||||||||||
| if maxResults <= 0 { | ||||||||||||
| maxResults = 5 | ||||||||||||
| } | ||||||||||||
| results, err := b.client.Search(collection, query, maxResults) | ||||||||||||
| if err != nil { | ||||||||||||
| return nil, err | ||||||||||||
| } | ||||||||||||
| out := make([]CollectionSearchResult, 0, len(results)) | ||||||||||||
| for _, r := range results { | ||||||||||||
| out = append(out, CollectionSearchResult{ | ||||||||||||
| ID: r.ID, | ||||||||||||
| Content: r.Content, | ||||||||||||
| Metadata: r.Metadata, | ||||||||||||
| Similarity: r.Similarity, | ||||||||||||
| }) | ||||||||||||
| } | ||||||||||||
| return out, nil | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) Reset(collection string) error { | ||||||||||||
| return b.client.Reset(collection) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) DeleteEntry(collection, entry string) ([]string, error) { | ||||||||||||
| return b.client.DeleteEntry(collection, entry) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) AddSource(collection, url string, intervalMin int) error { | ||||||||||||
| return b.client.AddSource(collection, url, intervalMin) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) RemoveSource(collection, url string) error { | ||||||||||||
| return b.client.RemoveSource(collection, url) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (b *collectionsBackendHTTP) ListSources(collection string) ([]CollectionSourceInfo, error) { | ||||||||||||
| srcs, err := b.client.ListSources(collection) | ||||||||||||
| if err != nil { | ||||||||||||
| return nil, err | ||||||||||||
| } | ||||||||||||
| out := make([]CollectionSourceInfo, 0, len(srcs)) | ||||||||||||
| for _, s := range srcs { | ||||||||||||
| var lastUpdate time.Time | ||||||||||||
| if s.LastUpdate != "" { | ||||||||||||
| lastUpdate, _ = time.Parse(time.RFC3339, s.LastUpdate) | ||||||||||||
|
||||||||||||
| lastUpdate, _ = time.Parse(time.RFC3339, s.LastUpdate) | |
| lastUpdate, err = time.Parse(time.RFC3339, s.LastUpdate) | |
| if err != nil { | |
| return nil, err | |
| } |
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EntryExists for the HTTP backend calls ListEntries and scans the full list on every upload. For large collections this adds an extra network round-trip and O(n) work per upload. Prefer relying on Store/Upload returning a conflict error (and mapping it to 409) or add a dedicated endpoint to check existence (or cache entries) to avoid listing the whole collection each time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UploadbuildstmpPath := filepath.Join(tmpDir, filename)from a user-controlled filename. Iffilenameis absolute or contains../path separators,Joincan escapetmpDirand write to arbitrary locations before the deferred cleanup runs. Sanitize the name (e.g.,filepath.Base+ reject path traversal/absolute paths) before constructingtmpPath.