feat(search): add structured filters to AI session list endpoint and frontend

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 03:34:54 +00:00
parent 1d5454c31b
commit c3afc7a059
6 changed files with 350 additions and 43 deletions

View File

@@ -11,6 +11,7 @@ CRUD and interaction endpoints for AI-powered troubleshooting sessions:
POST /ai-sessions/{id}/rate — Submit post-session rating
"""
import logging
from datetime import datetime
from typing import Annotated, Optional
from uuid import UUID
@@ -464,6 +465,13 @@ async def list_sessions(
session_status: Optional[str] = Query(None, alias="status"),
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
problem_domain: Optional[str] = Query(None),
matched_flow_id: Optional[UUID] = Query(None),
confidence_tier: Optional[str] = Query(None, pattern="^(guided|exploring|discovery)$"),
ticket_id: Optional[str] = Query(None),
date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None),
q: Optional[str] = Query(None, min_length=2, max_length=200),
):
"""List the current user's AI sessions (owned or picked up)."""
user_id_str = str(current_user.id)
@@ -482,6 +490,19 @@ async def list_sessions(
if session_status:
query = query.where(AISession.status == session_status)
if problem_domain:
query = query.where(AISession.problem_domain == problem_domain)
if matched_flow_id:
query = query.where(AISession.matched_flow_id == matched_flow_id)
if confidence_tier:
query = query.where(AISession.confidence_tier == confidence_tier)
if ticket_id:
query = query.where(AISession.ticket_id == ticket_id)
if date_from:
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
result = await db.execute(query)
sessions = result.scalars().all()

View File

@@ -424,18 +424,43 @@ async def export_session(
for sd in sd_result.scalars().all()
]
# Query file upload evidence for non-PDF formats
from app.models.file_upload import FileUpload
from app.services import storage_service as _storage_service
from app.core.config import settings as _export_settings
upload_items: list[dict] = []
if _export_settings.STORAGE_ENDPOINT:
try:
uploads_result = await db.execute(
select(FileUpload)
.where(FileUpload.session_id == session_id)
.order_by(FileUpload.created_at)
)
for u in uploads_result.scalars().all():
try:
url = _storage_service.get_presigned_url(u.storage_key)
upload_items.append({
"filename": u.filename,
"url": url,
"is_image": u.content_type.startswith("image/"),
})
except Exception:
pass # Skip uploads that fail URL generation
except Exception:
pass # Storage errors should not fail the export
# Generate export based on format
if export_options.format == "markdown":
content = generate_markdown_export(session, export_options, supporting_data=supporting_data_items)
content = generate_markdown_export(session, export_options, supporting_data=supporting_data_items, uploads=upload_items)
media_type = "text/markdown"
elif export_options.format == "html":
content = generate_html_export(session, export_options, supporting_data=supporting_data_items)
content = generate_html_export(session, export_options, supporting_data=supporting_data_items, uploads=upload_items)
media_type = "text/html"
elif export_options.format == "psa":
content = generate_psa_export(session, export_options, supporting_data=supporting_data_items)
content = generate_psa_export(session, export_options, supporting_data=supporting_data_items, uploads=upload_items)
media_type = "text/plain"
else: # text
content = generate_text_export(session, export_options, supporting_data=supporting_data_items)
content = generate_text_export(session, export_options, supporting_data=supporting_data_items, uploads=upload_items)
media_type = "text/plain"
# Resolve variables in export output