671 lines
19 KiB
Go
671 lines
19 KiB
Go
// Package synap provides a production-ready Go client for the Synap cognitive memory system.
|
|
//
|
|
// Synap is a biologically-inspired memory database with spreading activation, episodic memory,
|
|
// and automatic consolidation. This client is designed for use in chat systems to provide
|
|
// context-aware AI responses with memory continuity.
|
|
//
|
|
// Usage Example:
|
|
//
|
|
// client, err := synap.NewClient("http://localhost:7432", &synap.Config{
|
|
// Timeout: 10 * time.Second,
|
|
// MaxRetries: 3,
|
|
// DefaultSpace: "conversation_abc123",
|
|
// })
|
|
// if err != nil {
|
|
// log.Fatal(err)
|
|
// }
|
|
//
|
|
// // Store a chat message as episodic memory
|
|
// episode := &synap.Episode{
|
|
// What: "User asked about their favorite hobbies",
|
|
// When: time.Now(),
|
|
// Who: []string{"user_alice", "agent_sarah"},
|
|
// Where: "chat",
|
|
// Confidence: 0.85,
|
|
// }
|
|
// memoryID, err := client.RememberEpisode(ctx, episode)
|
|
//
|
|
// // Recall relevant memories for context
|
|
// memories, err := client.Recall(ctx, &synap.RecallRequest{
|
|
// Query: "hobbies photography travel",
|
|
// Mode: synap.RecallModeHybrid,
|
|
// MaxResults: 10,
|
|
// Threshold: 0.5,
|
|
// })
|
|
package synap
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.threesix.ai/jordan/persona-community-3/pkg/httpclient"
|
|
)
|
|
|
|
// Client provides access to the Synap memory system.
|
|
type Client struct {
|
|
baseURL string
|
|
httpClient *httpclient.Client
|
|
logger *slog.Logger
|
|
config *Config
|
|
}
|
|
|
|
// Config holds configuration for the Synap client.
|
|
type Config struct {
|
|
// Timeout for HTTP requests (default: 10s)
|
|
Timeout time.Duration
|
|
|
|
// MaxRetries for failed requests (default: 3)
|
|
MaxRetries int
|
|
|
|
// DefaultSpace for multi-tenant isolation (optional)
|
|
// If set, all requests will use this space unless overridden
|
|
DefaultSpace string
|
|
|
|
// APIKey for authentication (optional)
|
|
// If set, requests will include "Authorization: Bearer <key>" header
|
|
APIKey string
|
|
|
|
// Logger for structured logging (optional)
|
|
Logger *slog.Logger
|
|
}
|
|
|
|
// NewClient creates a new Synap client.
|
|
//
|
|
// baseURL should be the Synap server URL (e.g., "http://localhost:7432")
|
|
// config is optional - pass nil to use defaults
|
|
//
|
|
// Returns nil and an error if baseURL is invalid.
|
|
func NewClient(baseURL string, config *Config) (*Client, error) {
|
|
if baseURL == "" {
|
|
return nil, fmt.Errorf("baseURL is required")
|
|
}
|
|
|
|
// Validate baseURL format
|
|
parsedURL, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid baseURL: %w", err)
|
|
}
|
|
|
|
if parsedURL.Scheme == "" {
|
|
return nil, fmt.Errorf("baseURL must include scheme (http:// or https://)")
|
|
}
|
|
|
|
if parsedURL.Host == "" {
|
|
return nil, fmt.Errorf("baseURL must include host")
|
|
}
|
|
|
|
if config == nil {
|
|
config = &Config{}
|
|
}
|
|
|
|
if config.Timeout == 0 {
|
|
config.Timeout = 10 * time.Second
|
|
}
|
|
|
|
if config.MaxRetries == 0 {
|
|
config.MaxRetries = 3
|
|
}
|
|
|
|
if config.Logger == nil {
|
|
config.Logger = slog.Default()
|
|
}
|
|
|
|
return &Client{
|
|
baseURL: baseURL,
|
|
httpClient: httpclient.New(httpclient.Config{
|
|
Timeout: config.Timeout,
|
|
MaxRetries: config.MaxRetries,
|
|
Logger: config.Logger,
|
|
}),
|
|
logger: config.Logger,
|
|
config: config,
|
|
}, nil
|
|
}
|
|
|
|
// Episode represents an episodic memory (what/when/where/who/why/how).
|
|
//
|
|
// Episodic memories are rich contextual memories that capture events with
|
|
// multiple dimensions. They consolidate over time into semantic patterns.
|
|
type Episode struct {
|
|
// What happened (required)
|
|
What string `json:"what"`
|
|
|
|
// When it happened (required for temporal context)
|
|
When time.Time `json:"when"`
|
|
|
|
// Where it happened (optional, e.g., "chat", "email", "phone")
|
|
Where string `json:"where,omitempty"`
|
|
|
|
// Who was involved (optional, e.g., ["user_alice", "agent_sarah"])
|
|
Who []string `json:"who,omitempty"`
|
|
|
|
// Why it happened (optional, context/motivation)
|
|
Why string `json:"why,omitempty"`
|
|
|
|
// How it happened (optional, method/process)
|
|
How string `json:"how,omitempty"`
|
|
|
|
// Confidence in memory accuracy (0.0-1.0, default: 0.7)
|
|
// Higher confidence = stronger memory encoding
|
|
Confidence float64 `json:"confidence"`
|
|
|
|
// Tags for categorization (optional)
|
|
Tags []string `json:"tags,omitempty"`
|
|
}
|
|
|
|
// RememberEpisodeResponse contains the result of storing an episode.
|
|
type RememberEpisodeResponse struct {
|
|
// MemoryID is the unique identifier for this episode
|
|
MemoryID string `json:"memory_id"`
|
|
|
|
// StorageConfidence reflects Synap's assessment of storage quality
|
|
StorageConfidence ConfidenceScore `json:"storage_confidence"`
|
|
|
|
// ConsolidationState indicates memory age ("Recent", "Consolidating", "Semantic")
|
|
ConsolidationState string `json:"consolidation_state"`
|
|
|
|
// ObservedAt is when the event originally occurred
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
|
|
// StoredAt is when Synap stored the memory
|
|
StoredAt time.Time `json:"stored_at"`
|
|
|
|
// SystemMessage provides feedback about storage
|
|
SystemMessage string `json:"system_message"`
|
|
}
|
|
|
|
// ConfidenceScore represents Synap's confidence in a memory or recall.
|
|
type ConfidenceScore struct {
|
|
// Value is the numeric confidence (0.0-1.0)
|
|
Value float64 `json:"value"`
|
|
|
|
// Category is the human-readable category ("Low", "Medium", "High", "Very High")
|
|
Category string `json:"category"`
|
|
|
|
// Reasoning explains how confidence was calculated (optional)
|
|
Reasoning string `json:"reasoning,omitempty"`
|
|
}
|
|
|
|
// RememberResponse contains the result of storing a simple semantic memory.
|
|
type RememberResponse struct {
|
|
// MemoryID is the unique identifier for this memory
|
|
MemoryID string `json:"memory_id"`
|
|
|
|
// ObservedAt is when the memory was created
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
|
|
// StoredAt is when Synap stored the memory
|
|
StoredAt time.Time `json:"stored_at"`
|
|
|
|
// StorageConfidence reflects Synap's assessment of storage quality
|
|
StorageConfidence ConfidenceScore `json:"storage_confidence"`
|
|
}
|
|
|
|
// Remember stores a simple semantic memory in Synap.
|
|
//
|
|
// This is a convenience method for storing straightforward facts or observations
|
|
// without the full episodic context (what/when/where/who/why/how).
|
|
// For richer contextual memories, use RememberEpisode instead.
|
|
//
|
|
// The memory will be stored in the space specified in the client config,
|
|
// or you can override it with WithSpace in the context.
|
|
//
|
|
// Example:
|
|
//
|
|
// memoryID, err := client.Remember(ctx, "User loves hiking and photography", 0.85)
|
|
func (c *Client) Remember(ctx context.Context, content string, confidence float64) (*RememberResponse, error) {
|
|
if content == "" {
|
|
return nil, fmt.Errorf("content is required")
|
|
}
|
|
|
|
if confidence == 0 {
|
|
confidence = 0.7 // Default confidence
|
|
}
|
|
|
|
space := getSpace(ctx, c.config.DefaultSpace)
|
|
endpoint := c.buildURL("/api/v1/memories/remember", map[string]string{
|
|
"space": space,
|
|
})
|
|
|
|
reqBody := map[string]interface{}{
|
|
"content": content,
|
|
"confidence": confidence,
|
|
}
|
|
|
|
body, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if c.config.APIKey != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
|
}
|
|
|
|
c.logger.Debug("storing semantic memory in synap",
|
|
"content_length", len(content),
|
|
"space", space,
|
|
"confidence", confidence)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
bodyBytes, readErr := io.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, fmt.Errorf("synap API error (HTTP %d): failed to read error body: %w", resp.StatusCode, readErr)
|
|
}
|
|
return nil, fmt.Errorf("synap API error (HTTP %d): %s", resp.StatusCode, bodyBytes)
|
|
}
|
|
|
|
var result RememberResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
|
|
c.logger.Info("semantic memory stored in synap",
|
|
"memory_id", result.MemoryID,
|
|
"confidence", result.StorageConfidence.Value,
|
|
"space", space)
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// RememberEpisode stores an episodic memory in Synap.
|
|
//
|
|
// The memory will be stored in the space specified in the client config,
|
|
// or you can override it with WithSpace in the context.
|
|
//
|
|
// Example:
|
|
//
|
|
// episode := &synap.Episode{
|
|
// What: "User mentioned they love hiking and photography",
|
|
// When: time.Now(),
|
|
// Who: []string{"user_alice"},
|
|
// Confidence: 0.85,
|
|
// }
|
|
// memoryID, err := client.RememberEpisode(ctx, episode)
|
|
func (c *Client) RememberEpisode(ctx context.Context, episode *Episode) (*RememberEpisodeResponse, error) {
|
|
if episode.What == "" {
|
|
return nil, fmt.Errorf("episode.What is required")
|
|
}
|
|
|
|
if episode.When.IsZero() {
|
|
return nil, fmt.Errorf("episode.When is required")
|
|
}
|
|
|
|
if episode.Confidence == 0 {
|
|
episode.Confidence = 0.7 // Default confidence
|
|
}
|
|
|
|
space := getSpace(ctx, c.config.DefaultSpace)
|
|
endpoint := c.buildURL("/api/v1/episodes/remember", map[string]string{
|
|
"space": space,
|
|
})
|
|
|
|
body, err := json.Marshal(episode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal episode: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if c.config.APIKey != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
|
}
|
|
|
|
c.logger.Debug("storing episode in synap",
|
|
"what", episode.What,
|
|
"space", space,
|
|
"confidence", episode.Confidence)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
bodyBytes, readErr := io.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, fmt.Errorf("synap API error (HTTP %d): failed to read error body: %w", resp.StatusCode, readErr)
|
|
}
|
|
return nil, fmt.Errorf("synap API error (HTTP %d): %s", resp.StatusCode, bodyBytes)
|
|
}
|
|
|
|
var result RememberEpisodeResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
|
|
c.logger.Info("episode stored in synap",
|
|
"memory_id", result.MemoryID,
|
|
"confidence", result.StorageConfidence.Value,
|
|
"space", space)
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// RecallMode specifies how memories should be retrieved.
|
|
type RecallMode string
|
|
|
|
const (
|
|
// RecallModeSimilarity uses vector similarity search only
|
|
RecallModeSimilarity RecallMode = "similarity"
|
|
|
|
// RecallModeSpreading uses spreading activation through memory graph
|
|
RecallModeSpreading RecallMode = "spreading"
|
|
|
|
// RecallModeHybrid combines similarity and spreading (recommended for chat)
|
|
RecallModeHybrid RecallMode = "hybrid"
|
|
)
|
|
|
|
// RecallRequest specifies parameters for memory recall.
|
|
type RecallRequest struct {
|
|
// Query is the natural language search query (required)
|
|
Query string
|
|
|
|
// Mode specifies recall strategy (default: hybrid)
|
|
Mode RecallMode
|
|
|
|
// MaxResults limits the number of memories returned (default: 10)
|
|
MaxResults int
|
|
|
|
// Threshold is the minimum confidence for results (0.0-1.0, default: 0.5)
|
|
Threshold float64
|
|
|
|
// FromTime filters memories after this time (optional)
|
|
FromTime *time.Time
|
|
|
|
// ToTime filters memories before this time (optional)
|
|
ToTime *time.Time
|
|
|
|
// RequiredTags filters to memories with these tags (optional)
|
|
RequiredTags []string
|
|
|
|
// ExcludedTags filters out memories with these tags (optional)
|
|
ExcludedTags []string
|
|
|
|
// TraceActivation returns activation spreading details (for debugging)
|
|
TraceActivation bool
|
|
}
|
|
|
|
// Memory represents a recalled memory from Synap.
|
|
type Memory struct {
|
|
// ID is the unique memory identifier
|
|
ID string `json:"id"`
|
|
|
|
// Content is the memory content (for semantic memories)
|
|
// For episodes, use Episode field instead
|
|
Content string `json:"content,omitempty"`
|
|
|
|
// Episode contains episodic memory details (what/when/where/who/why/how)
|
|
Episode *EpisodeDetails `json:"episode,omitempty"`
|
|
|
|
// Confidence in the memory accuracy
|
|
Confidence ConfidenceScore `json:"confidence"`
|
|
|
|
// ActivationLevel shows how "awake" this memory is (0.0-1.0)
|
|
// Higher activation = more recently/frequently accessed
|
|
ActivationLevel float64 `json:"activation_level"`
|
|
|
|
// SimilarityScore shows relevance to the query (0.0-1.0)
|
|
SimilarityScore float64 `json:"similarity_score"`
|
|
|
|
// ObservedAt is when the original event occurred
|
|
ObservedAt time.Time `json:"observed_at,omitempty"`
|
|
|
|
// Tags categorize the memory
|
|
Tags []string `json:"tags,omitempty"`
|
|
}
|
|
|
|
// EpisodeDetails contains the rich context of an episodic memory.
|
|
type EpisodeDetails struct {
|
|
What string `json:"what"`
|
|
When time.Time `json:"when"`
|
|
Where string `json:"where,omitempty"`
|
|
Who []string `json:"who,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
How string `json:"how,omitempty"`
|
|
}
|
|
|
|
// RecallResponse contains memories recalled from Synap.
|
|
type RecallResponse struct {
|
|
// Memories organized by vividness
|
|
Memories MemoryCategories `json:"memories"`
|
|
|
|
// RecallConfidence indicates overall recall quality
|
|
RecallConfidence ConfidenceScore `json:"recall_confidence"`
|
|
|
|
// QueryAnalysis provides insights into the search
|
|
QueryAnalysis QueryAnalysis `json:"query_analysis"`
|
|
|
|
// SystemMessage provides feedback
|
|
SystemMessage string `json:"system_message"`
|
|
}
|
|
|
|
// MemoryCategories groups memories by their vividness/confidence.
|
|
type MemoryCategories struct {
|
|
// Vivid memories are high-confidence, directly relevant
|
|
Vivid []Memory `json:"vivid"`
|
|
|
|
// Associated memories are related through spreading activation
|
|
Associated []Memory `json:"associated"`
|
|
|
|
// Reconstructed memories are pattern-completed from partial information
|
|
Reconstructed []Memory `json:"reconstructed"`
|
|
}
|
|
|
|
// QueryAnalysis provides insights into how the query was processed.
|
|
type QueryAnalysis struct {
|
|
// UnderstoodIntent is Synap's interpretation of the query
|
|
UnderstoodIntent string `json:"understood_intent"`
|
|
|
|
// SearchStrategy describes the retrieval approach
|
|
SearchStrategy string `json:"search_strategy"`
|
|
|
|
// CognitiveLoad indicates query complexity ("Low", "Medium", "High")
|
|
CognitiveLoad string `json:"cognitive_load"`
|
|
|
|
// Suggestions for improving recall (optional)
|
|
Suggestions []string `json:"suggestions,omitempty"`
|
|
}
|
|
|
|
// Recall retrieves relevant memories from Synap.
|
|
//
|
|
// Example:
|
|
//
|
|
// memories, err := client.Recall(ctx, &synap.RecallRequest{
|
|
// Query: "hobbies and interests",
|
|
// Mode: synap.RecallModeHybrid,
|
|
// MaxResults: 10,
|
|
// Threshold: 0.5,
|
|
// })
|
|
//
|
|
// for _, memory := range memories.Memories.Vivid {
|
|
// fmt.Printf("Memory: %s (confidence: %.2f)\n", memory.Content, memory.Confidence.Value)
|
|
// }
|
|
func (c *Client) Recall(ctx context.Context, req *RecallRequest) (*RecallResponse, error) {
|
|
if req.Query == "" {
|
|
return nil, fmt.Errorf("query is required")
|
|
}
|
|
|
|
if req.Mode == "" {
|
|
req.Mode = RecallModeHybrid
|
|
}
|
|
|
|
if req.MaxResults == 0 {
|
|
req.MaxResults = 10
|
|
}
|
|
|
|
if req.Threshold == 0 {
|
|
req.Threshold = 0.5
|
|
}
|
|
|
|
space := getSpace(ctx, c.config.DefaultSpace)
|
|
|
|
// Build query parameters
|
|
params := map[string]string{
|
|
"query": req.Query,
|
|
"mode": string(req.Mode),
|
|
"max_results": fmt.Sprintf("%d", req.MaxResults),
|
|
"threshold": fmt.Sprintf("%.2f", req.Threshold),
|
|
"space": space,
|
|
}
|
|
|
|
if req.FromTime != nil {
|
|
params["from_time"] = req.FromTime.Format(time.RFC3339)
|
|
}
|
|
|
|
if req.ToTime != nil {
|
|
params["to_time"] = req.ToTime.Format(time.RFC3339)
|
|
}
|
|
|
|
if len(req.RequiredTags) > 0 {
|
|
params["required_tags"] = strings.Join(req.RequiredTags, ",")
|
|
}
|
|
|
|
if len(req.ExcludedTags) > 0 {
|
|
params["excluded_tags"] = strings.Join(req.ExcludedTags, ",")
|
|
}
|
|
|
|
if req.TraceActivation {
|
|
params["trace_activation"] = "true"
|
|
}
|
|
|
|
endpoint := c.buildURL("/api/v1/memories/recall", params)
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
if c.config.APIKey != "" {
|
|
httpReq.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
|
}
|
|
|
|
c.logger.Debug("recalling memories from synap",
|
|
"query", req.Query,
|
|
"mode", req.Mode,
|
|
"space", space)
|
|
|
|
resp, err := c.httpClient.Do(httpReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
bodyBytes, readErr := io.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, fmt.Errorf("synap API error (HTTP %d): failed to read error body: %w", resp.StatusCode, readErr)
|
|
}
|
|
return nil, fmt.Errorf("synap API error (HTTP %d): %s", resp.StatusCode, bodyBytes)
|
|
}
|
|
|
|
var result RecallResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
|
|
totalMemories := len(result.Memories.Vivid) + len(result.Memories.Associated) + len(result.Memories.Reconstructed)
|
|
c.logger.Info("recalled memories from synap",
|
|
"total", totalMemories,
|
|
"vivid", len(result.Memories.Vivid),
|
|
"associated", len(result.Memories.Associated),
|
|
"confidence", result.RecallConfidence.Value,
|
|
"space", space)
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// Health checks if the Synap server is reachable and healthy.
|
|
func (c *Client) Health(ctx context.Context) error {
|
|
endpoint := c.baseURL + "/api/v1/system/health"
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
if c.config.APIKey != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
bodyBytes, readErr := io.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return fmt.Errorf("synap health check failed (HTTP %d): failed to read error body: %w", resp.StatusCode, readErr)
|
|
}
|
|
return fmt.Errorf("synap health check failed (HTTP %d): %s", resp.StatusCode, bodyBytes)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// buildURL constructs a URL with query parameters.
|
|
func (c *Client) buildURL(path string, params map[string]string) string {
|
|
u, _ := url.Parse(c.baseURL + path)
|
|
|
|
q := u.Query()
|
|
for k, v := range params {
|
|
if v != "" {
|
|
q.Set(k, v)
|
|
}
|
|
}
|
|
|
|
u.RawQuery = q.Encode()
|
|
return u.String()
|
|
}
|
|
|
|
// Context keys for per-request configuration.
|
|
type contextKey string
|
|
|
|
const spaceContextKey contextKey = "synap_space"
|
|
|
|
// WithSpace returns a context with a specific memory space override.
|
|
//
|
|
// This overrides the default space configured in the client for a single request.
|
|
//
|
|
// Example:
|
|
//
|
|
// ctx := synap.WithSpace(ctx, "conversation_xyz")
|
|
// client.RememberEpisode(ctx, episode) // Uses conversation_xyz space
|
|
func WithSpace(ctx context.Context, space string) context.Context {
|
|
return context.WithValue(ctx, spaceContextKey, space)
|
|
}
|
|
|
|
// getSpace retrieves the space from context, falling back to default.
|
|
func getSpace(ctx context.Context, defaultSpace string) string {
|
|
if space, ok := ctx.Value(spaceContextKey).(string); ok && space != "" {
|
|
return space
|
|
}
|
|
return defaultSpace
|
|
}
|