feat(search): add semantic similar session matching via Voyage AI embeddings

Adds vector-based similar session discovery using the existing Voyage AI
embedding infrastructure and pgvector cosine similarity search.

- New AISessionEmbedding model with vector(1024) column
- session_embedding_service: generate + upsert embeddings, find similar sessions
- Embeddings generated on session create (from problem_summary/domain) and
  updated on resolve (adds resolution_summary)
- GET /ai-sessions/{id}/similar endpoint returns top-N similar sessions
- Migration a7c9e3b1f402 creates ai_session_embeddings table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 03:48:09 +00:00
parent ce68fa84ca
commit e356103408
6 changed files with 343 additions and 0 deletions

View File

@@ -48,6 +48,7 @@ from .notification_log import NotificationLog
from .notification import Notification
from .psa_activity_log import PsaActivityLog
from .file_upload import FileUpload
from .ai_session_embedding import AISessionEmbedding
__all__ = [
"User",
@@ -110,4 +111,5 @@ __all__ = [
"Notification",
"PsaActivityLog",
"FileUpload",
"AISessionEmbedding",
]

View File

@@ -0,0 +1,53 @@
"""AI session embedding storage for similar-session matching.
Stores vector embeddings of AI session content (problem summary, resolution,
domain) for cosine similarity search via pgvector. One embedding per session.
"""
import uuid
from datetime import datetime, timezone
from sqlalchemy import String, Text, DateTime, ForeignKey, Index
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID
from app.core.database import Base
try:
from pgvector.sqlalchemy import Vector
except ImportError:
Vector = None
class AISessionEmbedding(Base):
__tablename__ = "ai_session_embeddings"
__table_args__ = (
Index("ix_ai_session_embeddings_account_id", "account_id"),
Index("ix_ai_session_embeddings_session_id", "session_id", unique=True),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
session_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("ai_sessions.id", ondelete="CASCADE"),
nullable=False,
)
account_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("accounts.id", ondelete="CASCADE"),
nullable=False,
)
chunk_text: Mapped[str] = mapped_column(Text, nullable=False)
embedding_model: Mapped[str] = mapped_column(
String(50), nullable=False, default="voyage-3.5"
)
# The embedding column is created via migration with vector(1024) type
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)