memory

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 19, 2026 License: GPL-3.0 Imports: 2 Imported by: 0

Documentation

Overview

Package memory defines the three-layer memory architecture used by Glyphoxa NPC agents.

The architecture is organised as a hierarchy of increasing abstraction:

  • L1 – Session Store (SessionStore): hot, time-ordered transcript log. Allows fast writes and recency-window retrieval during an active session.
  • L2 – Semantic Index (SemanticIndex): vector store for embedding-based similarity search over chunked transcript content.
  • L3 – Knowledge Graph (KnowledgeGraph / GraphRAGQuerier): a graph of named entities and typed relationships, supporting multi-hop traversal and graph-augmented retrieval (GraphRAG).

All interfaces are public so that external packages can supply alternative storage backends (Postgres/pgvector, Redis, Neo4j, in-memory, …) without depending on glyphoxa internals.

Every implementation must be safe for concurrent use.

Index

Constants

View Source
const (
	AttrOccupation    = "occupation"
	AttrAppearance    = "appearance"
	AttrSpeakingStyle = "speaking_style"
	AttrPersonality   = "personality"
	AttrAlignment     = "alignment"
)

Well-known entity attribute keys. These are not exhaustive — entities may carry arbitrary keys — but these are recognised by the formatter and other subsystems for special ordering or display treatment.

Variables

This section is empty.

Functions

This section is empty.

Types

type Chunk

type Chunk struct {
	// ID is the unique identifier for this chunk (e.g., a UUID).
	ID string

	// SessionID is the session this chunk belongs to.
	SessionID string

	// Content is the raw text of the chunk (may be a sentence, paragraph, or utterance).
	Content string

	// Embedding is the vector representation of Content.
	// Dimension must match the index configuration (e.g., 1536 for OpenAI
	// text-embedding-3-small).
	Embedding []float32

	// SpeakerID identifies who produced this chunk.
	SpeakerID string

	// EntityID is the knowledge-graph entity associated with this chunk
	// (NPC, location, item, etc.). Used for GraphRAG scoping.
	EntityID string

	// Topic is an optional coarse topic label (e.g., "quest", "trade", "lore").
	Topic string

	// Timestamp is when this chunk was recorded.
	Timestamp time.Time
}

Chunk is a processed segment of transcript content prepared for semantic indexing (L2). A Chunk carries its pre-computed embedding so the index does not need to re-embed on insertion.

type ChunkFilter

type ChunkFilter struct {
	// SessionID restricts results to a single session.
	SessionID string

	// SpeakerID restricts results to chunks produced by a specific speaker.
	SpeakerID string

	// EntityID restricts results to chunks associated with a specific entity.
	EntityID string

	// After filters chunks recorded after this instant (exclusive).
	After time.Time

	// Before filters chunks recorded before this instant (exclusive).
	Before time.Time
}

ChunkFilter narrows a semantic search to a subset of indexed chunks (L2). All non-zero fields are applied as AND conditions.

type ChunkResult

type ChunkResult struct {
	// Chunk is the retrieved segment.
	Chunk Chunk

	// Distance is the vector-space distance to the query embedding
	// (e.g., cosine distance or L2 — interpretation is implementation-defined).
	Distance float64
}

ChunkResult pairs a retrieved chunk with its vector-space distance from the query embedding (L2). Lower Distance values indicate higher semantic similarity.

type ContextResult

type ContextResult struct {
	// Entity is the knowledge-graph node that anchors this result.
	Entity Entity

	// Content is the retrieved text passage relevant to the query.
	Content string

	// Score is the combined retrieval relevance score (0.0–1.0, higher is better).
	Score float64
}

ContextResult pairs a knowledge-graph entity with retrieved textual content that is relevant to a [GraphRAGQuerier.QueryWithContext] call.

type Entity

type Entity struct {
	// ID is the unique, stable identifier for this entity (e.g., a UUID).
	ID string

	// Type classifies the entity.
	// Recommended values: npc, player, location, item, faction, event, quest, concept.
	// Custom values are allowed.
	Type string

	// Name is the canonical display name (e.g., "Eldrinax the Undying").
	Name string

	// Attributes holds arbitrary key/value metadata specific to this entity
	// (e.g., alignment, health, occupation, description).
	Attributes map[string]any

	// CreatedAt is when the entity was first added to the graph.
	CreatedAt time.Time

	// UpdatedAt is when the entity was last modified.
	UpdatedAt time.Time
}

Entity represents a named object in the knowledge graph (L3). Entities are typed nodes; their dynamic attributes are stored in a free-form map to accommodate the diversity of tabletop RPG settings.

type EntityFilter

type EntityFilter struct {
	// Type restricts results to entities of this type. Empty matches all types.
	Type string

	// Name restricts results to entities whose name contains this substring
	// (case-insensitive). Empty matches all names.
	Name string

	// AttributeQuery is a map of attribute keys to required values.
	// An entity matches if every key/value pair in AttributeQuery is present
	// in its Attributes map.
	AttributeQuery map[string]any
}

EntityFilter specifies predicates for entity lookup queries. All non-zero fields are applied as AND conditions.

type GraphRAGQuerier

type GraphRAGQuerier interface {
	KnowledgeGraph

	// QueryWithContext performs a GraphRAG query using full-text search (FTS):
	// it matches the query string against chunk content using PostgreSQL
	// plainto_tsquery, scoped to entities in graphScope.
	// Results are ranked by FTS relevance (ts_rank).
	//
	// graphScope limits results to chunks whose entity association is in the
	// list. An empty graphScope searches all chunks.
	//
	// Use this when no embedding vector is available. For higher-quality
	// semantic retrieval, prefer [GraphRAGQuerier.QueryWithEmbedding].
	QueryWithContext(ctx context.Context, query string, graphScope []string) ([]ContextResult, error)

	// QueryWithEmbedding performs a GraphRAG query using vector similarity:
	// it finds the topK chunks whose embeddings are closest (cosine distance)
	// to the provided query embedding, scoped to entities in graphScope.
	// Results are ranked by ascending cosine distance (most similar first).
	//
	// graphScope limits results to chunks whose entity association is in the
	// list. An empty graphScope searches all chunks.
	//
	// The embedding must match the dimensionality of stored chunk embeddings.
	// topK controls the maximum number of results returned.
	QueryWithEmbedding(ctx context.Context, embedding []float32, topK int, graphScope []string) ([]ContextResult, error)
}

GraphRAGQuerier extends KnowledgeGraph with graph-augmented retrieval (GraphRAG). It combines structured graph traversal with semantic text retrieval to produce contextually grounded results for LLM consumption.

Two query methods are provided:

  • [GraphRAGQuerier.QueryWithContext] uses full-text search (FTS) and requires no embedding provider. Useful as a fallback when embeddings are unavailable or during entity extraction where embedding budget is limited.
  • [GraphRAGQuerier.QueryWithEmbedding] uses pgvector cosine similarity against pre-computed chunk embeddings. This is the true GraphRAG path described in the design docs and produces higher-quality results when an embedding provider is available.

type KnowledgeGraph

type KnowledgeGraph interface {
	// AddEntity upserts an entity into the graph.
	// If an entity with the same ID already exists it is completely replaced.
	AddEntity(ctx context.Context, entity Entity) error

	// GetEntity retrieves an entity by its unique ID.
	// Returns (nil, nil) when the entity does not exist.
	GetEntity(ctx context.Context, id string) (*Entity, error)

	// UpdateEntity merges attrs into the Attributes map of the specified entity
	// and refreshes its UpdatedAt timestamp. Keys present in attrs overwrite
	// existing values; absent keys are left unchanged.
	// Returns an error when the entity does not exist.
	UpdateEntity(ctx context.Context, id string, attrs map[string]any) error

	// DeleteEntity removes the entity and all its associated relationships from
	// the graph. Deleting a non-existent entity is not an error.
	DeleteEntity(ctx context.Context, id string) error

	// FindEntities returns all entities matching filter.
	// Returns an empty (non-nil) slice when no entities match.
	FindEntities(ctx context.Context, filter EntityFilter) ([]Entity, error)

	// AddRelationship upserts a directed edge between two entities.
	// If a relationship with the same (SourceID, TargetID, RelType) already
	// exists it is completely replaced.
	AddRelationship(ctx context.Context, rel Relationship) error

	// GetRelationships returns relationships associated with entityID.
	// By default only outgoing edges are returned; use [WithIncoming] to include
	// inbound edges, and [WithRelTypes] to filter by edge type.
	// Returns an empty (non-nil) slice when no relationships match.
	GetRelationships(ctx context.Context, entityID string, opts ...RelQueryOpt) ([]Relationship, error)

	// DeleteRelationship removes the directed edge identified by (sourceID,
	// targetID, relType). Deleting a non-existent edge is not an error.
	DeleteRelationship(ctx context.Context, sourceID, targetID, relType string) error

	// Neighbors performs a breadth-first traversal from entityID up to depth
	// hops and returns all reachable entities (the start entity is excluded).
	// [TraversalOpt] options can restrict which edge or node types are followed.
	// Returns an empty (non-nil) slice when no neighbours are reachable.
	Neighbors(ctx context.Context, entityID string, depth int, opts ...TraversalOpt) ([]Entity, error)

	// FindPath returns the shortest sequence of entities connecting fromID to
	// toID inclusive, following directed edges up to maxDepth hops.
	// Returns an empty (non-nil) slice when no path exists within maxDepth.
	FindPath(ctx context.Context, fromID, toID string, maxDepth int) ([]Entity, error)

	// VisibleSubgraph returns the subset of the graph visible from the
	// perspective of npcID: the NPC node itself, all entities it has direct
	// relationships with, and those relationships.
	// Implementations may apply visibility rules (e.g., only publicly known facts).
	VisibleSubgraph(ctx context.Context, npcID string) ([]Entity, []Relationship, error)

	// IdentitySnapshot assembles a compact [NPCIdentity] for npcID, suitable for
	// injecting into a system prompt or context window.
	IdentitySnapshot(ctx context.Context, npcID string) (*NPCIdentity, error)
}

KnowledgeGraph is the L3 memory layer: a graph of named Entity nodes connected by typed Relationship edges.

It supports full CRUD on nodes and edges, multi-hop neighbourhood traversal, shortest-path queries, and NPC-specific projection methods.

Mutating operations that act on a primary key (AddEntity, AddRelationship) must behave as upserts rather than returning an error on duplicates. Deletions of non-existent records are not errors.

Implementations must be safe for concurrent use.

type NPCIdentity

type NPCIdentity struct {
	// Entity is the NPC's own node in the knowledge graph.
	Entity Entity

	// Relationships are all direct edges from/to this NPC.
	Relationships []Relationship

	// RelatedEntities are the entities connected to this NPC by Relationships.
	RelatedEntities []Entity
}

NPCIdentity is a compact snapshot of an NPC's knowledge-graph identity, suitable for injection into a system prompt or context window.

type Provenance

type Provenance struct {
	// SessionID is the game session during which this fact was established.
	SessionID string

	// Timestamp is when the fact was established.
	Timestamp time.Time

	// Confidence is the model's confidence in this fact (0.0–1.0).
	Confidence float64

	// Source describes how the fact was derived.
	// Well-known values: "stated" (directly spoken), "inferred" (model reasoning).
	Source string

	// DMConfirmed indicates a human Dungeon Master has validated this fact.
	DMConfirmed bool
}

Provenance records the origin of a fact asserted in the knowledge graph. It is embedded in Relationship to allow downstream reasoning about reliability.

type Recap added in v0.1.0

type Recap struct {
	// SessionID is the session this recap was generated from.
	SessionID string

	// CampaignID identifies the campaign.
	CampaignID string

	// Text is the dramatic narrative recap text.
	Text string

	// AudioData is the rendered PCM audio bytes.
	AudioData []byte

	// SampleRate is the audio sample rate in Hz.
	SampleRate int

	// Channels is the number of audio channels.
	Channels int

	// Duration is the estimated speech duration.
	Duration time.Duration

	// GeneratedAt is when this recap was created.
	GeneratedAt time.Time
}

Recap is the generated "Previously On..." voiced summary for a session.

type RecapStore added in v0.1.0

type RecapStore interface {
	// SaveRecap persists a recap. If a recap for the same session already
	// exists it is replaced (upsert).
	SaveRecap(ctx context.Context, recap Recap) error

	// GetRecap retrieves the recap for the given session.
	// Returns (nil, nil) when no recap exists.
	GetRecap(ctx context.Context, sessionID string) (*Recap, error)
}

RecapStore persists and retrieves generated session recaps. Implementations must be safe for concurrent use.

type RelQueryOpt

type RelQueryOpt func(*relQueryOptions)

RelQueryOpt is a functional option for [KnowledgeGraph.GetRelationships].

func WithIncoming

func WithIncoming() RelQueryOpt

WithIncoming includes relationships where the queried entity is the target (i.e., inbound edges). By default only outgoing relationships are returned.

func WithOutgoing

func WithOutgoing() RelQueryOpt

WithOutgoing includes relationships where the queried entity is the source (i.e., outbound edges). This is the default behaviour; calling it explicitly is a no-op but improves readability when combined with WithIncoming.

func WithRelLimit

func WithRelLimit(n int) RelQueryOpt

WithRelLimit caps the number of relationships returned. A value of 0 means the implementation may apply its own default.

func WithRelTypes

func WithRelTypes(relTypes ...string) RelQueryOpt

WithRelTypes restricts the returned relationships to those whose RelType is in the provided list. An empty list (the default) returns all types.

type RelQueryParams

type RelQueryParams struct {
	RelTypes     []string
	DirectionIn  bool
	DirectionOut bool
	Limit        int
}

RelQueryParams holds the resolved parameters from a slice of RelQueryOpt.

func ApplyRelQueryOpts

func ApplyRelQueryOpts(opts []RelQueryOpt) RelQueryParams

ApplyRelQueryOpts applies a slice of RelQueryOpt functional options and returns the resolved query parameters as a RelQueryParams. This helper allows external packages (such as storage backends) to read the option values without needing to access the unexported [relQueryOptions] type directly.

type Relationship

type Relationship struct {
	// SourceID is the ID of the originating entity.
	SourceID string

	// TargetID is the ID of the destination entity.
	TargetID string

	// RelType is the semantic label of the relationship
	// (e.g., "knows", "hates", "owns", "member_of").
	RelType string

	// Attributes holds additional edge metadata
	// (e.g., since, strength, public, description).
	Attributes map[string]any

	// Provenance records the evidence trail for this relationship.
	Provenance Provenance

	// CreatedAt is when this relationship was first added.
	CreatedAt time.Time
}

Relationship is a directed, typed edge between two entities in the knowledge graph (L3).

type SearchOpts

type SearchOpts struct {
	// SessionID restricts the search to a single session.
	// An empty string searches across all sessions.
	SessionID string

	// After filters entries recorded after this instant (exclusive).
	// A zero Time disables the lower bound.
	After time.Time

	// Before filters entries recorded before this instant (exclusive).
	// A zero Time disables the upper bound.
	Before time.Time

	// SpeakerID restricts results to a specific speaker.
	// An empty string matches all speakers.
	SpeakerID string

	// Limit caps the number of results returned.
	// A value of 0 means the implementation may apply its own default.
	Limit int
}

SearchOpts configures a keyword / full-text search over session entries (L1). All non-zero fields are applied as AND conditions.

type SemanticIndex

type SemanticIndex interface {
	// IndexChunk stores a pre-embedded [Chunk] in the vector index.
	// If a chunk with the same ID already exists it must be replaced (upsert).
	IndexChunk(ctx context.Context, chunk Chunk) error

	// Search finds the topK chunks whose embeddings are closest to the query
	// embedding, filtered by filter.
	// Results are ordered by ascending Distance (most similar first).
	// Returns an empty (non-nil) slice when no chunks match.
	Search(ctx context.Context, embedding []float32, topK int, filter ChunkFilter) ([]ChunkResult, error)
}

SemanticIndex is the L2 memory layer: a vector store for embedding-based similarity search over chunked transcript content.

Callers are responsible for producing embeddings before calling IndexChunk or Search. Implementations must be safe for concurrent use.

type SessionInfo added in v0.1.0

type SessionInfo struct {
	// SessionID is the unique session identifier.
	SessionID string

	// CampaignID identifies the campaign this session belongs to.
	CampaignID string

	// StartedAt is when the session was started.
	StartedAt time.Time

	// EndedAt is when the session ended. Zero value if still active.
	EndedAt time.Time
}

SessionInfo holds metadata about a recorded session.

type SessionStore

type SessionStore interface {
	// WriteEntry appends a TranscriptEntry to the store for the given session.
	// sessionID must be non-empty.
	// Returns an error only on persistent storage failure.
	WriteEntry(ctx context.Context, sessionID string, entry TranscriptEntry) error

	// GetRecent returns all entries for the given session whose Timestamp is
	// no earlier than time.Now()-duration.
	// Returns an empty (non-nil) slice when no matching entries exist.
	GetRecent(ctx context.Context, sessionID string, duration time.Duration) ([]TranscriptEntry, error)

	// Search performs keyword / full-text search over stored entries.
	// The query string is matched against the Text field.
	// opts refines the result set by time range, speaker, or session scope.
	// Returns an empty (non-nil) slice when no entries match.
	Search(ctx context.Context, query string, opts SearchOpts) ([]TranscriptEntry, error)

	// EntryCount returns the total number of transcript entries stored for
	// the given session. Returns 0 when the session has no entries.
	EntryCount(ctx context.Context, sessionID string) (int, error)

	// ListSessions returns sessions for the current campaign, newest first.
	// limit caps the number of results (0 = implementation default).
	// Returns an empty (non-nil) slice when no sessions exist.
	ListSessions(ctx context.Context, limit int) ([]SessionInfo, error)
}

SessionStore is the L1 memory layer: a time-ordered, append-only log of TranscriptEntry records for one or more game sessions.

Entries must be returned in chronological order unless otherwise specified. Implementations must be safe for concurrent use.

type SpeakerRole added in v0.1.1

type SpeakerRole string

SpeakerRole classifies the speaker within the session context.

const (
	// RoleDefault is the zero-value role for ordinary players.
	RoleDefault SpeakerRole = ""

	// RoleGM identifies the Game Master (DM) player.
	RoleGM SpeakerRole = "gm"

	// RoleGMAssistant identifies an NPC acting as the GM's AI assistant.
	RoleGMAssistant SpeakerRole = "gm_assistant"
)

func (SpeakerRole) DisplaySuffix added in v0.1.1

func (r SpeakerRole) DisplaySuffix() string

DisplaySuffix returns a human-readable label suitable for appending to speaker names in formatted transcripts. Returns "" for RoleDefault.

type TranscriptEntry

type TranscriptEntry struct {
	// SpeakerID identifies who spoke (player user ID or NPC name).
	SpeakerID string

	// SpeakerName is the human-readable speaker name.
	SpeakerName string

	// SpeakerRole classifies the speaker (e.g., GM, GM assistant).
	// The zero value means ordinary player.
	SpeakerRole SpeakerRole

	// Text is the (possibly corrected) transcript text.
	Text string

	// RawText is the original uncorrected STT output. Preserved for debugging.
	RawText string

	// NPCID identifies the NPC agent that produced this entry.
	// Empty for non-NPC (e.g. player) entries.
	NPCID string

	// Timestamp is when this entry was recorded.
	Timestamp time.Time

	// Duration is the length of the utterance.
	Duration time.Duration
}

TranscriptEntry is a complete exchange record written to the session log. It captures both the speaker's utterance and optionally the NPC's response, forming the atomic unit of session history.

func (TranscriptEntry) IsNPC

func (e TranscriptEntry) IsNPC() bool

IsNPC reports whether this entry was produced by an NPC agent.

type TraversalOpt

type TraversalOpt func(*traversalOptions)

TraversalOpt is a functional option for [KnowledgeGraph.Neighbors] graph traversals.

func TraverseMaxNodes

func TraverseMaxNodes(n int) TraversalOpt

TraverseMaxNodes caps the number of entities returned during a traversal. A value of 0 means the implementation may apply its own default.

func TraverseNodeTypes

func TraverseNodeTypes(nodeTypes ...string) TraversalOpt

TraverseNodeTypes restricts traversal to entity nodes whose Type is in the provided list. An empty list (the default) visits all node types.

func TraverseRelTypes

func TraverseRelTypes(relTypes ...string) TraversalOpt

TraverseRelTypes restricts traversal to edges whose RelType is in the provided list. An empty list (the default) follows all edge types.

type TraversalParams

type TraversalParams struct {
	RelTypes  []string
	NodeTypes []string
	MaxNodes  int
}

TraversalParams holds the resolved parameters from a slice of TraversalOpt.

func ApplyTraversalOpts

func ApplyTraversalOpts(opts []TraversalOpt) TraversalParams

ApplyTraversalOpts applies a slice of TraversalOpt functional options and returns the resolved traversal parameters as a TraversalParams. This helper allows external packages to read the option values without accessing the unexported [traversalOptions] type.

Directories

Path Synopsis
Package export provides campaign export and import as .tar.gz archives.
Package export provides campaign export and import as .tar.gz archives.
Package mock provides in-memory test doubles for the memory layer interfaces.
Package mock provides in-memory test doubles for the memory layer interfaces.
Package postgres provides a PostgreSQL-backed implementation of the three-layer Glyphoxa memory architecture (L1 session log, L2 semantic index, L3 knowledge graph).
Package postgres provides a PostgreSQL-backed implementation of the three-layer Glyphoxa memory architecture (L1 session log, L2 semantic index, L3 knowledge graph).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL