v2 restructure: Go client, Docker engine, release tooling

- Remove v1 Python CLI (src/kb_search/, tests/, root pyproject.toml, uv.lock, .venv)
- Add Go client with cross-platform build (client/)
- Add FastAPI engine with NVIDIA and multi-stage ROCm Dockerfiles (engine/)
- Add VERSION files for client and engine, wired into builds
- Add release.sh for automated build, tag, release, and Docker push
- Update README with build/release docs and ROCm migration note
- Clean up .gitignore for v2 project structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 21:52:25 +00:00
parent 2030976b85
commit 9aab79d49b
98 changed files with 4526 additions and 7776 deletions
+158
View File
@@ -0,0 +1,158 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"github.com/kb-search/kb/internal/config"
)
// FileUpload describes a file to include in a multipart request.
type FileUpload struct {
FieldName string
FileName string
Reader io.Reader
}
// Client is an HTTP client for the kb-search engine API.
type Client struct {
baseURL string
apiKey string
httpClient *http.Client
}
// NewClient creates a Client from the current configuration.
func NewClient() *Client {
cfg := config.Get()
return &Client{
baseURL: cfg.EngineURL,
apiKey: cfg.APIKey,
httpClient: &http.Client{},
}
}
func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request, error) {
url := c.baseURL + path
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
if c.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+c.apiKey)
}
return req, nil
}
func (c *Client) do(req *http.Request) (*http.Response, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("Cannot reach engine at %s: %v", c.baseURL, err)
}
return resp, nil
}
// Get performs a GET request to the given path.
func (c *Client) Get(path string) (*http.Response, error) {
req, err := c.newRequest(http.MethodGet, path, nil)
if err != nil {
return nil, err
}
return c.do(req)
}
// Post performs a POST request with a JSON body.
func (c *Client) Post(path string, body interface{}) (*http.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
req, err := c.newRequest(http.MethodPost, path, bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
return c.do(req)
}
// PostMultipart performs a POST request with multipart/form-data encoding.
func (c *Client) PostMultipart(path string, fields map[string]string, file *FileUpload) (*http.Response, error) {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
for k, v := range fields {
if err := w.WriteField(k, v); err != nil {
return nil, fmt.Errorf("failed to write field %s: %w", k, err)
}
}
if file != nil {
part, err := w.CreateFormFile(file.FieldName, file.FileName)
if err != nil {
return nil, fmt.Errorf("failed to create form file: %w", err)
}
if _, err := io.Copy(part, file.Reader); err != nil {
return nil, fmt.Errorf("failed to copy file data: %w", err)
}
}
if err := w.Close(); err != nil {
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
}
req, err := c.newRequest(http.MethodPost, path, &buf)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", w.FormDataContentType())
return c.do(req)
}
// Delete performs a DELETE request to the given path.
func (c *Client) Delete(path string) (*http.Response, error) {
req, err := c.newRequest(http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
return c.do(req)
}
// Put performs a PUT request with a JSON body.
func (c *Client) Put(path string, body interface{}) (*http.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
req, err := c.newRequest(http.MethodPut, path, bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
return c.do(req)
}
// DecodeJSON reads the response body and decodes it into target.
func DecodeJSON(resp *http.Response, target interface{}) error {
defer resp.Body.Close()
return json.NewDecoder(resp.Body).Decode(target)
}
// CheckError returns a formatted error for non-2xx responses, or nil on success.
func CheckError(resp *http.Response) error {
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var errResp struct {
Detail string `json:"detail"`
}
if json.Unmarshal(body, &errResp) == nil && errResp.Detail != "" {
return fmt.Errorf("API error (%d): %s", resp.StatusCode, errResp.Detail)
}
return fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body))
}