Files
HCFS/sdks/go/hcfs.go
2025-07-30 09:34:16 +10:00

740 lines
21 KiB
Go

// Package hcfs provides a Go SDK for the Context-Aware Hierarchical Context File System
package hcfs
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
)
// Version of the HCFS Go SDK
const Version = "2.0.0"
// Default configuration values
const (
DefaultTimeout = 30 * time.Second
DefaultRetries = 3
DefaultRateLimit = 100 // requests per second
DefaultCacheSize = 1000
DefaultCacheTTL = time.Hour
)
// ContextStatus represents the status of a context
type ContextStatus string
const (
ContextStatusActive ContextStatus = "active"
ContextStatusArchived ContextStatus = "archived"
ContextStatusDeleted ContextStatus = "deleted"
ContextStatusDraft ContextStatus = "draft"
)
// SearchType represents the type of search to perform
type SearchType string
const (
SearchTypeSemantic SearchType = "semantic"
SearchTypeKeyword SearchType = "keyword"
SearchTypeHybrid SearchType = "hybrid"
SearchTypeFuzzy SearchType = "fuzzy"
)
// Context represents a context in the HCFS system
type Context struct {
ID *int `json:"id,omitempty"`
Path string `json:"path"`
Content string `json:"content"`
Summary *string `json:"summary,omitempty"`
Author *string `json:"author,omitempty"`
Tags []string `json:"tags,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Status *ContextStatus `json:"status,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Version *int `json:"version,omitempty"`
SimilarityScore *float64 `json:"similarity_score,omitempty"`
}
// ContextCreate represents data for creating a new context
type ContextCreate struct {
Path string `json:"path"`
Content string `json:"content"`
Summary *string `json:"summary,omitempty"`
Author *string `json:"author,omitempty"`
Tags []string `json:"tags,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// ContextUpdate represents data for updating a context
type ContextUpdate struct {
Content *string `json:"content,omitempty"`
Summary *string `json:"summary,omitempty"`
Tags []string `json:"tags,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Status *ContextStatus `json:"status,omitempty"`
}
// SearchResult represents a search result
type SearchResult struct {
Context Context `json:"context"`
Score float64 `json:"score"`
Explanation *string `json:"explanation,omitempty"`
Highlights []string `json:"highlights,omitempty"`
}
// SearchOptions represents search configuration options
type SearchOptions struct {
SearchType *SearchType `json:"search_type,omitempty"`
TopK *int `json:"top_k,omitempty"`
SimilarityThreshold *float64 `json:"similarity_threshold,omitempty"`
PathPrefix *string `json:"path_prefix,omitempty"`
SemanticWeight *float64 `json:"semantic_weight,omitempty"`
IncludeContent *bool `json:"include_content,omitempty"`
IncludeHighlights *bool `json:"include_highlights,omitempty"`
MaxHighlights *int `json:"max_highlights,omitempty"`
}
// ContextFilter represents filtering options for listing contexts
type ContextFilter struct {
PathPrefix *string `json:"path_prefix,omitempty"`
Author *string `json:"author,omitempty"`
Status *ContextStatus `json:"status,omitempty"`
Tags []string `json:"tags,omitempty"`
CreatedAfter *time.Time `json:"created_after,omitempty"`
CreatedBefore *time.Time `json:"created_before,omitempty"`
ContentContains *string `json:"content_contains,omitempty"`
MinContentLength *int `json:"min_content_length,omitempty"`
MaxContentLength *int `json:"max_content_length,omitempty"`
}
// PaginationOptions represents pagination configuration
type PaginationOptions struct {
Page *int `json:"page,omitempty"`
PageSize *int `json:"page_size,omitempty"`
SortBy *string `json:"sort_by,omitempty"`
SortOrder *string `json:"sort_order,omitempty"`
}
// PaginationMeta represents pagination metadata
type PaginationMeta struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalItems int `json:"total_items"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrevious bool `json:"has_previous"`
}
// BatchResult represents the result of a batch operation
type BatchResult struct {
SuccessCount int `json:"success_count"`
ErrorCount int `json:"error_count"`
TotalItems int `json:"total_items"`
SuccessfulItems []interface{} `json:"successful_items"`
FailedItems []map[string]interface{} `json:"failed_items"`
ExecutionTime time.Duration `json:"execution_time"`
SuccessRate float64 `json:"success_rate"`
}
// APIResponse represents a generic API response wrapper
type APIResponse[T any] struct {
Success bool `json:"success"`
Data T `json:"data"`
Timestamp time.Time `json:"timestamp"`
APIVersion string `json:"api_version"`
RequestID *string `json:"request_id,omitempty"`
}
// ListResponse represents a paginated list response
type ListResponse[T any] struct {
APIResponse[[]T]
Pagination PaginationMeta `json:"pagination"`
}
// SearchResponse represents a search response
type SearchResponse struct {
Success bool `json:"success"`
Data []SearchResult `json:"data"`
Query string `json:"query"`
SearchType SearchType `json:"search_type"`
TotalResults int `json:"total_results"`
SearchTimeMs float64 `json:"search_time_ms"`
FiltersApplied map[string]any `json:"filters_applied,omitempty"`
Timestamp time.Time `json:"timestamp"`
APIVersion string `json:"api_version"`
}
// HealthStatus represents health status
type HealthStatus string
const (
HealthStatusHealthy HealthStatus = "healthy"
HealthStatusDegraded HealthStatus = "degraded"
HealthStatusUnhealthy HealthStatus = "unhealthy"
)
// ComponentHealth represents health of a system component
type ComponentHealth struct {
Name string `json:"name"`
Status HealthStatus `json:"status"`
ResponseTimeMs *float64 `json:"response_time_ms,omitempty"`
ErrorMessage *string `json:"error_message,omitempty"`
}
// HealthResponse represents health check response
type HealthResponse struct {
Status HealthStatus `json:"status"`
Version string `json:"version"`
UptimeSeconds float64 `json:"uptime_seconds"`
Components []ComponentHealth `json:"components"`
}
// Config represents HCFS client configuration
type Config struct {
BaseURL string
APIKey string
JWTToken string
Timeout time.Duration
UserAgent string
MaxRetries int
RetryDelay time.Duration
RateLimit float64
MaxConcurrentRequests int
CacheEnabled bool
CacheSize int
CacheTTL time.Duration
}
// DefaultConfig returns a default configuration
func DefaultConfig(baseURL, apiKey string) *Config {
return &Config{
BaseURL: baseURL,
APIKey: apiKey,
Timeout: DefaultTimeout,
UserAgent: fmt.Sprintf("hcfs-go/%s", Version),
MaxRetries: DefaultRetries,
RetryDelay: time.Second,
RateLimit: DefaultRateLimit,
MaxConcurrentRequests: 100,
CacheEnabled: true,
CacheSize: DefaultCacheSize,
CacheTTL: DefaultCacheTTL,
}
}
// Client represents the HCFS API client
type Client struct {
config *Config
httpClient *http.Client
rateLimiter *rate.Limiter
cache *cache
analytics *analytics
mu sync.RWMutex
}
// NewClient creates a new HCFS client
func NewClient(config *Config) *Client {
if config == nil {
panic("config cannot be nil")
}
client := &Client{
config: config,
httpClient: &http.Client{
Timeout: config.Timeout,
},
rateLimiter: rate.NewLimiter(rate.Limit(config.RateLimit), int(config.RateLimit)),
analytics: newAnalytics(),
}
if config.CacheEnabled {
client.cache = newCache(config.CacheSize, config.CacheTTL)
}
return client
}
// HealthCheck checks the API health status
func (c *Client) HealthCheck(ctx context.Context) (*HealthResponse, error) {
var response HealthResponse
err := c.request(ctx, "GET", "/health", nil, nil, &response)
if err != nil {
return nil, err
}
return &response, nil
}
// CreateContext creates a new context
func (c *Client) CreateContext(ctx context.Context, contextData *ContextCreate) (*Context, error) {
if contextData == nil {
return nil, fmt.Errorf("contextData cannot be nil")
}
if !validatePath(contextData.Path) {
return nil, fmt.Errorf("invalid context path: %s", contextData.Path)
}
// Normalize path
contextData.Path = normalizePath(contextData.Path)
var response APIResponse[Context]
err := c.request(ctx, "POST", "/api/v1/contexts", nil, contextData, &response)
if err != nil {
return nil, err
}
// Invalidate relevant cache entries
c.invalidateCache("/api/v1/contexts")
return &response.Data, nil
}
// GetContext retrieves a context by ID
func (c *Client) GetContext(ctx context.Context, contextID int) (*Context, error) {
path := fmt.Sprintf("/api/v1/contexts/%d", contextID)
// Check cache first
if c.cache != nil {
if cached, ok := c.cache.get(path); ok {
if context, ok := cached.(*Context); ok {
c.analytics.recordCacheHit()
return context, nil
}
}
c.analytics.recordCacheMiss()
}
var response APIResponse[Context]
err := c.request(ctx, "GET", path, nil, nil, &response)
if err != nil {
return nil, err
}
// Cache the result
if c.cache != nil {
c.cache.set(path, &response.Data)
}
return &response.Data, nil
}
// ListContexts lists contexts with filtering and pagination
func (c *Client) ListContexts(ctx context.Context, filter *ContextFilter, pagination *PaginationOptions) ([]Context, *PaginationMeta, error) {
params := url.Values{}
// Add filter parameters
if filter != nil {
if filter.PathPrefix != nil {
params.Set("path_prefix", *filter.PathPrefix)
}
if filter.Author != nil {
params.Set("author", *filter.Author)
}
if filter.Status != nil {
params.Set("status", string(*filter.Status))
}
if filter.CreatedAfter != nil {
params.Set("created_after", filter.CreatedAfter.Format(time.RFC3339))
}
if filter.CreatedBefore != nil {
params.Set("created_before", filter.CreatedBefore.Format(time.RFC3339))
}
if filter.ContentContains != nil {
params.Set("content_contains", *filter.ContentContains)
}
if filter.MinContentLength != nil {
params.Set("min_content_length", strconv.Itoa(*filter.MinContentLength))
}
if filter.MaxContentLength != nil {
params.Set("max_content_length", strconv.Itoa(*filter.MaxContentLength))
}
}
// Add pagination parameters
if pagination != nil {
if pagination.Page != nil {
params.Set("page", strconv.Itoa(*pagination.Page))
}
if pagination.PageSize != nil {
params.Set("page_size", strconv.Itoa(*pagination.PageSize))
}
if pagination.SortBy != nil {
params.Set("sort_by", *pagination.SortBy)
}
if pagination.SortOrder != nil {
params.Set("sort_order", *pagination.SortOrder)
}
}
var response ListResponse[Context]
err := c.request(ctx, "GET", "/api/v1/contexts", params, nil, &response)
if err != nil {
return nil, nil, err
}
return response.Data, &response.Pagination, nil
}
// UpdateContext updates an existing context
func (c *Client) UpdateContext(ctx context.Context, contextID int, updates *ContextUpdate) (*Context, error) {
if updates == nil {
return nil, fmt.Errorf("updates cannot be nil")
}
path := fmt.Sprintf("/api/v1/contexts/%d", contextID)
var response APIResponse[Context]
err := c.request(ctx, "PUT", path, nil, updates, &response)
if err != nil {
return nil, err
}
// Invalidate cache
c.invalidateCache(path)
c.invalidateCache("/api/v1/contexts")
return &response.Data, nil
}
// DeleteContext deletes a context
func (c *Client) DeleteContext(ctx context.Context, contextID int) error {
path := fmt.Sprintf("/api/v1/contexts/%d", contextID)
err := c.request(ctx, "DELETE", path, nil, nil, nil)
if err != nil {
return err
}
// Invalidate cache
c.invalidateCache(path)
c.invalidateCache("/api/v1/contexts")
return nil
}
// SearchContexts searches contexts using various search methods
func (c *Client) SearchContexts(ctx context.Context, query string, options *SearchOptions) ([]SearchResult, error) {
if query == "" {
return nil, fmt.Errorf("query cannot be empty")
}
searchData := map[string]interface{}{
"query": query,
}
if options != nil {
if options.SearchType != nil {
searchData["search_type"] = string(*options.SearchType)
}
if options.TopK != nil {
searchData["top_k"] = *options.TopK
}
if options.SimilarityThreshold != nil {
searchData["similarity_threshold"] = *options.SimilarityThreshold
}
if options.PathPrefix != nil {
searchData["path_prefix"] = *options.PathPrefix
}
if options.SemanticWeight != nil {
searchData["semantic_weight"] = *options.SemanticWeight
}
if options.IncludeContent != nil {
searchData["include_content"] = *options.IncludeContent
}
if options.IncludeHighlights != nil {
searchData["include_highlights"] = *options.IncludeHighlights
}
if options.MaxHighlights != nil {
searchData["max_highlights"] = *options.MaxHighlights
}
}
var response SearchResponse
err := c.request(ctx, "POST", "/api/v1/search", nil, searchData, &response)
if err != nil {
return nil, err
}
return response.Data, nil
}
// BatchCreateContexts creates multiple contexts in batch
func (c *Client) BatchCreateContexts(ctx context.Context, contexts []*ContextCreate) (*BatchResult, error) {
if len(contexts) == 0 {
return nil, fmt.Errorf("contexts cannot be empty")
}
startTime := time.Now()
// Validate and normalize all contexts
for _, context := range contexts {
if !validatePath(context.Path) {
return nil, fmt.Errorf("invalid context path: %s", context.Path)
}
context.Path = normalizePath(context.Path)
}
batchData := map[string]interface{}{
"contexts": contexts,
}
var response APIResponse[BatchResult]
err := c.request(ctx, "POST", "/api/v1/contexts/batch", nil, batchData, &response)
if err != nil {
return nil, err
}
// Calculate additional metrics
result := response.Data
result.ExecutionTime = time.Since(startTime)
result.SuccessRate = float64(result.SuccessCount) / float64(result.TotalItems)
// Invalidate cache
c.invalidateCache("/api/v1/contexts")
return &result, nil
}
// IterateContexts iterates through all contexts with automatic pagination
func (c *Client) IterateContexts(ctx context.Context, filter *ContextFilter, pageSize int, callback func(Context) error) error {
if pageSize <= 0 {
pageSize = 100
}
page := 1
for {
pagination := &PaginationOptions{
Page: &page,
PageSize: &pageSize,
}
contexts, paginationMeta, err := c.ListContexts(ctx, filter, pagination)
if err != nil {
return err
}
if len(contexts) == 0 {
break
}
for _, context := range contexts {
if err := callback(context); err != nil {
return err
}
}
// If we got fewer contexts than requested, we've reached the end
if len(contexts) < pageSize || !paginationMeta.HasNext {
break
}
page++
}
return nil
}
// GetAnalytics returns client analytics
func (c *Client) GetAnalytics() map[string]interface{} {
c.mu.RLock()
defer c.mu.RUnlock()
analytics := map[string]interface{}{
"session_start": c.analytics.sessionStart,
"operation_count": c.analytics.operationCount,
"error_count": c.analytics.errorCount,
"total_requests": c.analytics.totalRequests,
"failed_requests": c.analytics.failedRequests,
}
if c.cache != nil {
analytics["cache_stats"] = map[string]interface{}{
"enabled": true,
"size": c.cache.size(),
"max_size": c.cache.maxSize,
"hit_rate": c.analytics.getCacheHitRate(),
}
} else {
analytics["cache_stats"] = map[string]interface{}{
"enabled": false,
}
}
return analytics
}
// ClearCache clears the client cache
func (c *Client) ClearCache() {
if c.cache != nil {
c.cache.clear()
}
}
// Close closes the client and cleans up resources
func (c *Client) Close() error {
if c.cache != nil {
c.cache.clear()
}
return nil
}
// Internal method to make HTTP requests
func (c *Client) request(ctx context.Context, method, path string, params url.Values, body interface{}, result interface{}) error {
// Rate limiting
if err := c.rateLimiter.Wait(ctx); err != nil {
return fmt.Errorf("rate limit error: %w", err)
}
// Build URL
u, err := url.Parse(c.config.BaseURL + path)
if err != nil {
return fmt.Errorf("invalid URL: %w", err)
}
if params != nil {
u.RawQuery = params.Encode()
}
// Prepare body
var bodyReader io.Reader
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshal body: %w", err)
}
bodyReader = bytes.NewReader(bodyBytes)
}
// Create request
req, err := http.NewRequestWithContext(ctx, method, u.String(), bodyReader)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("User-Agent", c.config.UserAgent)
req.Header.Set("Content-Type", "application/json")
if c.config.APIKey != "" {
req.Header.Set("X-API-Key", c.config.APIKey)
}
if c.config.JWTToken != "" {
req.Header.Set("Authorization", "Bearer "+c.config.JWTToken)
}
// Execute request with retries
var resp *http.Response
for attempt := 0; attempt <= c.config.MaxRetries; attempt++ {
c.analytics.recordRequest()
resp, err = c.httpClient.Do(req)
if err != nil {
if attempt == c.config.MaxRetries {
c.analytics.recordError(err.Error())
return fmt.Errorf("request failed after %d attempts: %w", c.config.MaxRetries+1, err)
}
time.Sleep(c.config.RetryDelay * time.Duration(attempt+1))
continue
}
// Check if we should retry based on status code
if shouldRetry(resp.StatusCode) && attempt < c.config.MaxRetries {
resp.Body.Close()
time.Sleep(c.config.RetryDelay * time.Duration(attempt+1))
continue
}
break
}
defer resp.Body.Close()
// Handle error responses
if resp.StatusCode >= 400 {
c.analytics.recordError(fmt.Sprintf("HTTP %d", resp.StatusCode))
return c.handleHTTPError(resp)
}
// Parse response if result is provided
if result != nil {
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
}
return nil
}
// Handle HTTP errors and convert to appropriate error types
func (c *Client) handleHTTPError(resp *http.Response) error {
body, _ := io.ReadAll(resp.Body)
var errorResp struct {
Error string `json:"error"`
ErrorDetails []struct {
Field string `json:"field"`
Message string `json:"message"`
Code string `json:"code"`
} `json:"error_details"`
}
json.Unmarshal(body, &errorResp)
message := errorResp.Error
if message == "" {
message = fmt.Sprintf("HTTP %d error", resp.StatusCode)
}
switch resp.StatusCode {
case 400:
return &ValidationError{Message: message, Details: errorResp.ErrorDetails}
case 401:
return &AuthenticationError{Message: message}
case 404:
return &NotFoundError{Message: message}
case 429:
retryAfter := resp.Header.Get("Retry-After")
return &RateLimitError{Message: message, RetryAfter: retryAfter}
case 500, 502, 503, 504:
return &ServerError{Message: message, StatusCode: resp.StatusCode}
default:
return &APIError{Message: message, StatusCode: resp.StatusCode}
}
}
// Utility functions
func validatePath(path string) bool {
return strings.HasPrefix(path, "/") && !strings.Contains(path, "//")
}
func normalizePath(path string) string {
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
// Remove duplicate slashes
for strings.Contains(path, "//") {
path = strings.ReplaceAll(path, "//", "/")
}
return path
}
func (c *Client) invalidateCache(pattern string) {
if c.cache == nil {
return
}
c.cache.invalidatePattern(pattern)
}
func shouldRetry(statusCode int) bool {
return statusCode == 429 || statusCode >= 500
}