Files
poimen-workflows/internal/memory/client.go
T

297 lines
8.1 KiB
Go
Raw Normal View History

package memory
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Client memory service client with JWT auth
type Client struct {
baseURL string
httpClient *http.Client
token string
}
// NewClient creates memory service client
func NewClient(baseURL, token string) *Client {
return &Client{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
token: token,
}
}
// IngestRequest ingest knowledge record
type IngestRequest struct {
Project string `json:"project"`
Source string `json:"source"`
Kind string `json:"kind"` // L1|L2|reference
Text string `json:"text"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// IngestResponse ingest response
type IngestResponse struct {
ID string `json:"id"`
SHA256 string `json:"sha256"`
QueueStatus string `json:"queue_status"`
IdempotencyID string `json:"idempotency_key"`
}
// Ingest creates knowledge record
func (c *Client) Ingest(ctx context.Context, req *IngestRequest) (*IngestResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal ingest request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/memory/ingest", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
c.setAuthHeader(httpReq)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("ingest request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("ingest failed (%d): %s", resp.StatusCode, string(body))
}
var result IngestResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode ingest response: %w", err)
}
return &result, nil
}
// QueryRequest query memory
type QueryRequest struct {
Project string `json:"project"`
Query string `json:"query"`
LevelFilter []string `json:"level_filter,omitempty"` // L1, L2, R
Floor float32 `json:"floor,omitempty"`
Limit int `json:"limit,omitempty"`
Scope string `json:"scope,omitempty"` // learned|reference|all
}
// QueryResult single search result
type QueryResult struct {
ID string `json:"id"`
Level string `json:"level"`
Score float32 `json:"score"`
SemanticScore float32 `json:"semantic_score"`
LexicalScore float32 `json:"lexical_score"`
Text string `json:"text"`
Breadcrumb string `json:"breadcrumb"`
Source string `json:"source"`
}
// QueryResponse query response
type QueryResponse struct {
Query string `json:"query"`
Results []QueryResult `json:"results"`
TotalHits int `json:"total_hits"`
SearchTimeMS int `json:"search_time_ms"`
}
// Query searches knowledge
func (c *Client) Query(ctx context.Context, req *QueryRequest) (*QueryResponse, error) {
if req.Limit == 0 {
req.Limit = 10
}
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal query request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/memory/query", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
c.setAuthHeader(httpReq)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("query request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("query failed (%d): %s", resp.StatusCode, string(body))
}
var result QueryResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode query response: %w", err)
}
return &result, nil
}
// ContextRequest retrieve context (three-tier)
type ContextRequest struct {
Project string `json:"project"`
Tool string `json:"tool"`
Task string `json:"task"`
SignatureSource string `json:"signature_source"`
Scope string `json:"scope,omitempty"` // tool_context
Budget int `json:"budget,omitempty"`
}
// ContextLesson lesson from context
type ContextLesson struct {
Tier int `json:"tier"`
Level string `json:"level"`
Score float32 `json:"score"`
Text string `json:"text"`
MatchedKind string `json:"matched_kind,omitempty"`
SeenCount int `json:"seen_count,omitempty"`
LastSeen string `json:"last_seen,omitempty"`
}
// ContextSkill skill suggestion
type ContextSkill struct {
Name string `json:"name"`
Why string `json:"why"`
}
// ContextBudget budget tracking
type ContextBudget struct {
Requested int `json:"requested"`
Used int `json:"used"`
Dropped int `json:"dropped"`
Degradation *string `json:"degradation"`
}
// ContextResponse context response
type ContextResponse struct {
Tier int `json:"tier"`
Lessons []ContextLesson `json:"lessons"`
Skills []ContextSkill `json:"skills"`
Budget ContextBudget `json:"budget"`
}
// Context retrieves context (three-tier retrieval)
func (c *Client) Context(ctx context.Context, req *ContextRequest) (*ContextResponse, error) {
if req.Budget == 0 {
req.Budget = 8192
}
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal context request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/memory/context", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
c.setAuthHeader(httpReq)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("context request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("context failed (%d): %s", resp.StatusCode, string(body))
}
var result ContextResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode context response: %w", err)
}
return &result, nil
}
// VaultFile file in vault
type VaultFile struct {
Path string `json:"path"`
Title string `json:"title"`
Level string `json:"level"`
UpdatedAt string `json:"updated_at"`
RecordCount int `json:"record_count"`
}
// VaultResponse vault browse response
type VaultResponse struct {
Project string `json:"project"`
Files []VaultFile `json:"files"`
TotalRecords int `json:"total_records"`
}
// Vault browses vault files
func (c *Client) Vault(ctx context.Context, project string) (*VaultResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/memory/vault?project=%s", c.baseURL, project), nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
c.setAuthHeader(httpReq)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("vault request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("vault failed (%d): %s", resp.StatusCode, string(body))
}
var result VaultResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode vault response: %w", err)
}
return &result, nil
}
// setAuthHeader sets JWT Bearer token
func (c *Client) setAuthHeader(req *http.Request) {
if c.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
}
}
// Health checks memory service
func (c *Client) Health(ctx context.Context) (bool, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/health", nil)
if err != nil {
return false, err
}
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return false, err
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK, nil
}