feat(search): add PostgreSQL FTS on AI sessions with Command Palette integration
- Migration: add generated tsvector column + GIN index on ai_sessions (problem_summary, resolution_summary, escalation_reason, problem_domain)
- Backend: wire FTS into list_sessions q param; add GET /ai-sessions/search endpoint returning AISessionSearchResult (registered before /{session_id} to avoid UUID routing conflict)
- Frontend: add AISessionSearchResult type, aiSessionsApi.search() method, and Command Palette group "FlowPilot Sessions" using Zap icon navigating to /pilot/:id
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ from typing import Annotated, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from sqlalchemy import or_, select, func
|
||||
from sqlalchemy import or_, select, func, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -41,6 +41,7 @@ from app.schemas.ai_session import (
|
||||
AISessionSummary,
|
||||
AISessionDetail,
|
||||
AISessionStepResponse,
|
||||
AISessionSearchResult,
|
||||
StepOptionSchema,
|
||||
)
|
||||
from app.services import flowpilot_engine
|
||||
@@ -454,6 +455,44 @@ async def link_ticket_to_session(
|
||||
return detail
|
||||
|
||||
|
||||
# ── Search sessions (Command Palette) ──
|
||||
|
||||
@router.get("/search", response_model=list[AISessionSearchResult])
|
||||
@limiter.limit("30/minute")
|
||||
async def search_sessions(
|
||||
request: Request,
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
q: str = Query(..., min_length=2, max_length=200),
|
||||
limit: int = Query(5, ge=1, le=20),
|
||||
):
|
||||
"""Search AI sessions by content using full-text search. Used by Command Palette."""
|
||||
result = await db.execute(
|
||||
select(AISession)
|
||||
.where(
|
||||
or_(
|
||||
AISession.user_id == current_user.id,
|
||||
AISession.account_id == current_user.account_id,
|
||||
),
|
||||
text("ai_sessions.search_vector @@ plainto_tsquery('english', :q)"),
|
||||
)
|
||||
.params(q=q)
|
||||
.order_by(AISession.created_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
sessions = result.scalars().all()
|
||||
return [
|
||||
AISessionSearchResult(
|
||||
id=s.id,
|
||||
problem_summary=s.problem_summary,
|
||||
problem_domain=s.problem_domain,
|
||||
status=s.status,
|
||||
created_at=s.created_at,
|
||||
)
|
||||
for s in sessions
|
||||
]
|
||||
|
||||
|
||||
# ── List sessions ──
|
||||
|
||||
@router.get("", response_model=list[AISessionSummary])
|
||||
@@ -502,7 +541,10 @@ async def list_sessions(
|
||||
query = query.where(AISession.created_at >= date_from)
|
||||
if date_to:
|
||||
query = query.where(AISession.created_at <= date_to)
|
||||
# TODO: Full-text search via q param — see Task 7
|
||||
if q:
|
||||
query = query.where(
|
||||
text("ai_sessions.search_vector @@ plainto_tsquery('english', :q)")
|
||||
).params(q=q)
|
||||
|
||||
result = await db.execute(query)
|
||||
sessions = result.scalars().all()
|
||||
|
||||
@@ -190,3 +190,14 @@ class AISessionDetail(AISessionSummary):
|
||||
steps: list[AISessionStepResponse] = []
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class AISessionSearchResult(BaseModel):
|
||||
"""Lightweight session result for Command Palette / autocomplete."""
|
||||
id: UUID
|
||||
problem_summary: str | None = None
|
||||
problem_domain: str | None = None
|
||||
status: str
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
Reference in New Issue
Block a user