- Implement 12 Temporal activities for memory operations - Activities: create, update, search, context, diagnose, analyze, document - Add activity registration and worker setup - Full retry/timeout configuration with observability - Include workflow patterns and examples - All tests passing (23/23) Documentation: - MEMORY_INTEGRATION.md: High-level integration guide - MEMORY_ACTIVITIES.md: Complete activities reference - REGISTERED_ACTIVITIES.md: Registry and calling conventions
297 lines
8.1 KiB
Go
297 lines
8.1 KiB
Go
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
|
|
}
|