forked from cardosofelipe/fast-next-template
Add SQLAlchemy models for the Agent Memory System: - WorkingMemory: Key-value storage with TTL for active sessions - Episode: Experiential memories from task executions - Fact: Semantic knowledge triples with confidence scores - Procedure: Learned skills and procedures with success tracking - MemoryConsolidationLog: Tracks consolidation jobs between memory tiers Create enums for memory system: - ScopeType: global, project, agent_type, agent_instance, session - EpisodeOutcome: success, failure, partial - ConsolidationType: working_to_episodic, episodic_to_semantic, etc. - ConsolidationStatus: pending, running, completed, failed Add Alembic migration (0005) for all memory tables with: - Foreign key relationships to projects, agent_instances, agent_types - Comprehensive indexes for query patterns - Unique constraints for key lookups and triple uniqueness - Vector embedding column placeholders (Text fallback until pgvector enabled) Fix timezone-naive datetime.now() in types.py TaskState (review feedback) Includes 30 unit tests for models and enums. Closes #88 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
126 lines
3.6 KiB
Python
126 lines
3.6 KiB
Python
# app/models/memory/episode.py
|
|
"""
|
|
Episode database model.
|
|
|
|
Stores experiential memories - records of past task executions
|
|
with context, actions, outcomes, and lessons learned.
|
|
"""
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
Column,
|
|
DateTime,
|
|
Enum,
|
|
Float,
|
|
ForeignKey,
|
|
Index,
|
|
String,
|
|
Text,
|
|
)
|
|
from sqlalchemy.dialects.postgresql import (
|
|
JSONB,
|
|
UUID as PGUUID,
|
|
)
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from app.models.base import Base, TimestampMixin, UUIDMixin
|
|
|
|
from .enums import EpisodeOutcome
|
|
|
|
# Import pgvector type - will be available after migration enables extension
|
|
try:
|
|
from pgvector.sqlalchemy import Vector # type: ignore[import-not-found]
|
|
except ImportError:
|
|
# Fallback for environments without pgvector
|
|
Vector = None
|
|
|
|
|
|
class Episode(Base, UUIDMixin, TimestampMixin):
|
|
"""
|
|
Episodic memory model.
|
|
|
|
Records experiential memories from agent task execution:
|
|
- What task was performed
|
|
- What actions were taken
|
|
- What was the outcome
|
|
- What lessons were learned
|
|
"""
|
|
|
|
__tablename__ = "episodes"
|
|
|
|
# Foreign keys
|
|
project_id = Column(
|
|
PGUUID(as_uuid=True),
|
|
ForeignKey("projects.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
agent_instance_id = Column(
|
|
PGUUID(as_uuid=True),
|
|
ForeignKey("agent_instances.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
)
|
|
|
|
agent_type_id = Column(
|
|
PGUUID(as_uuid=True),
|
|
ForeignKey("agent_types.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
)
|
|
|
|
# Session reference
|
|
session_id = Column(String(255), nullable=False, index=True)
|
|
|
|
# Task information
|
|
task_type = Column(String(100), nullable=False, index=True)
|
|
task_description = Column(Text, nullable=False)
|
|
|
|
# Actions taken (list of action dictionaries)
|
|
actions = Column(JSONB, default=list, nullable=False)
|
|
|
|
# Context summary
|
|
context_summary = Column(Text, nullable=False)
|
|
|
|
# Outcome
|
|
outcome: Column[EpisodeOutcome] = Column(
|
|
Enum(EpisodeOutcome),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
outcome_details = Column(Text, nullable=True)
|
|
|
|
# Metrics
|
|
duration_seconds = Column(Float, nullable=False, default=0.0)
|
|
tokens_used = Column(BigInteger, nullable=False, default=0)
|
|
|
|
# Learning
|
|
lessons_learned = Column(JSONB, default=list, nullable=False)
|
|
importance_score = Column(Float, nullable=False, default=0.5, index=True)
|
|
|
|
# Vector embedding for semantic search
|
|
# Using 1536 dimensions for OpenAI text-embedding-3-small
|
|
embedding = Column(Vector(1536) if Vector else Text, nullable=True)
|
|
|
|
# When the episode occurred
|
|
occurred_at = Column(DateTime(timezone=True), nullable=False, index=True)
|
|
|
|
# Relationships
|
|
project = relationship("Project", foreign_keys=[project_id])
|
|
agent_instance = relationship("AgentInstance", foreign_keys=[agent_instance_id])
|
|
agent_type = relationship("AgentType", foreign_keys=[agent_type_id])
|
|
|
|
__table_args__ = (
|
|
# Primary query patterns
|
|
Index("ix_episodes_project_task", "project_id", "task_type"),
|
|
Index("ix_episodes_project_outcome", "project_id", "outcome"),
|
|
Index("ix_episodes_agent_task", "agent_instance_id", "task_type"),
|
|
Index("ix_episodes_project_time", "project_id", "occurred_at"),
|
|
# For importance-based pruning
|
|
Index("ix_episodes_importance_time", "importance_score", "occurred_at"),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Episode {self.id} task={self.task_type} outcome={self.outcome.value}>"
|