Files
fast-next-template/backend/app/models/memory/procedure.py
Felipe Cardoso bd988f76b0 fix(memory): address review findings from Issue #88
Fixes based on multi-agent review:

Model Improvements:
- Remove duplicate index ix_procedures_agent_type (already indexed via Column)
- Fix postgresql_where to use text() instead of string literal in Fact model
- Add thread-safety to Procedure.success_rate property (snapshot values)

Data Integrity Constraints:
- Add CheckConstraint for Episode: importance_score 0-1, duration >= 0, tokens >= 0
- Add CheckConstraint for Fact: confidence 0-1
- Add CheckConstraint for Procedure: success_count >= 0, failure_count >= 0

Migration Updates:
- Add check constraints creation in upgrade()
- Add check constraints removal in downgrade()

Note: SQLAlchemy Column default=list is correct (callable factory pattern)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 01:54:51 +01:00

130 lines
3.6 KiB
Python

# app/models/memory/procedure.py
"""
Procedure database model.
Stores procedural memories - learned skills and procedures
derived from successful task execution patterns.
"""
from sqlalchemy import (
CheckConstraint,
Column,
DateTime,
ForeignKey,
Index,
Integer,
String,
Text,
)
from sqlalchemy.dialects.postgresql import (
JSONB,
UUID as PGUUID,
)
from sqlalchemy.orm import relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
# Import pgvector type
try:
from pgvector.sqlalchemy import Vector # type: ignore[import-not-found]
except ImportError:
Vector = None
class Procedure(Base, UUIDMixin, TimestampMixin):
"""
Procedural memory model.
Stores learned procedures (skills) extracted from successful
task execution patterns:
- Name and trigger pattern for matching
- Step-by-step actions
- Success/failure tracking
"""
__tablename__ = "procedures"
# Scoping
project_id = Column(
PGUUID(as_uuid=True),
ForeignKey("projects.id", ondelete="CASCADE"),
nullable=True,
index=True,
)
agent_type_id = Column(
PGUUID(as_uuid=True),
ForeignKey("agent_types.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
# Procedure identification
name = Column(String(255), nullable=False, index=True)
trigger_pattern = Column(Text, nullable=False)
# Steps as JSON array of step objects
# Each step: {order, action, parameters, expected_outcome, fallback_action}
steps = Column(JSONB, default=list, nullable=False)
# Success tracking
success_count = Column(Integer, nullable=False, default=0)
failure_count = Column(Integer, nullable=False, default=0)
# Usage tracking
last_used = Column(DateTime(timezone=True), nullable=True, index=True)
# Vector embedding for semantic matching
embedding = Column(Vector(1536) if Vector else Text, nullable=True)
# Relationships
project = relationship("Project", foreign_keys=[project_id])
agent_type = relationship("AgentType", foreign_keys=[agent_type_id])
__table_args__ = (
# Unique procedure name within scope
Index(
"ix_procedures_unique_name",
"project_id",
"agent_type_id",
"name",
unique=True,
),
# Query patterns
Index("ix_procedures_project_name", "project_id", "name"),
# Note: agent_type_id already has index=True on Column definition
# For finding best procedures
Index("ix_procedures_success_rate", "success_count", "failure_count"),
# Data integrity constraints
CheckConstraint(
"success_count >= 0",
name="ck_procedures_success_positive",
),
CheckConstraint(
"failure_count >= 0",
name="ck_procedures_failure_positive",
),
)
@property
def success_rate(self) -> float:
"""Calculate the success rate of this procedure."""
# Snapshot values to avoid race conditions in concurrent access
success = self.success_count
failure = self.failure_count
total = success + failure
if total == 0:
return 0.0
return success / total
@property
def total_uses(self) -> int:
"""Get total number of times this procedure was used."""
# Snapshot values for consistency
return self.success_count + self.failure_count
def __repr__(self) -> str:
return (
f"<Procedure {self.name} ({self.id}) success_rate={self.success_rate:.2%}>"
)