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:
@@ -171,10 +171,10 @@ def _escape_markdown_table(value: str) -> str:
|
||||
return value.replace("|", "\\|").replace("\n", " ")
|
||||
|
||||
|
||||
def generate_markdown_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str:
|
||||
def generate_markdown_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None, uploads: list[dict] | None = None) -> str:
|
||||
"""Generate markdown export."""
|
||||
if _is_procedural_session(session):
|
||||
return _generate_procedural_markdown(session, options)
|
||||
return _generate_procedural_markdown(session, options, uploads=uploads)
|
||||
lines = []
|
||||
outcome_label = _get_outcome_label(session)
|
||||
|
||||
@@ -223,6 +223,21 @@ def generate_markdown_export(session: Session, options: SessionExport, supportin
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
# File upload evidence
|
||||
if uploads:
|
||||
lines.append("## Evidence")
|
||||
lines.append("")
|
||||
for upload in uploads:
|
||||
name = upload["filename"]
|
||||
url = upload["url"]
|
||||
if upload.get("is_image"):
|
||||
lines.append(f"- ")
|
||||
else:
|
||||
lines.append(f"- [{name}]({url})")
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
lines.append("## Troubleshooting Steps")
|
||||
lines.append("")
|
||||
|
||||
@@ -306,10 +321,10 @@ def generate_markdown_export(session: Session, options: SessionExport, supportin
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_text_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str:
|
||||
def generate_text_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None, uploads: list[dict] | None = None) -> str:
|
||||
"""Generate plain text export."""
|
||||
if _is_procedural_session(session):
|
||||
return _generate_procedural_text(session, options)
|
||||
return _generate_procedural_text(session, options, uploads=uploads)
|
||||
lines = []
|
||||
outcome_label = _get_outcome_label(session)
|
||||
|
||||
@@ -349,6 +364,13 @@ def generate_text_export(session: Session, options: SessionExport, supporting_da
|
||||
lines.append(scratchpad)
|
||||
lines.append("")
|
||||
|
||||
# File upload evidence
|
||||
if uploads:
|
||||
lines.append("--- Evidence ---")
|
||||
for upload in uploads:
|
||||
lines.append(f"- {upload['filename']}: {upload['url']}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("TROUBLESHOOTING STEPS")
|
||||
lines.append("-" * 20)
|
||||
|
||||
@@ -420,10 +442,10 @@ def generate_text_export(session: Session, options: SessionExport, supporting_da
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_html_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str:
|
||||
def generate_html_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None, uploads: list[dict] | None = None) -> str:
|
||||
"""Generate HTML export."""
|
||||
if _is_procedural_session(session):
|
||||
return _generate_procedural_html(session, options)
|
||||
return _generate_procedural_html(session, options, uploads=uploads)
|
||||
tree_name = html.escape(session.tree_snapshot.get("name", "Troubleshooting Session"))
|
||||
outcome_label = _get_outcome_label(session)
|
||||
|
||||
@@ -476,6 +498,19 @@ def generate_html_export(session: Session, options: SessionExport, supporting_da
|
||||
html_parts.append('<h2>Evidence / Reference</h2>')
|
||||
html_parts.append(f'<div class="scratchpad" style="white-space: pre-wrap; background: #f9f9f9; padding: 12px; border-radius: 4px; margin-bottom: 20px;">{html.escape(scratchpad)}</div>')
|
||||
|
||||
# File upload evidence
|
||||
if uploads:
|
||||
html_parts.append('<h3>Evidence</h3>')
|
||||
html_parts.append('<div class="evidence-grid" style="margin-bottom: 20px;">')
|
||||
for upload in uploads:
|
||||
name = html.escape(upload["filename"])
|
||||
url = html.escape(upload["url"])
|
||||
if upload.get("is_image"):
|
||||
html_parts.append(f'<img src="{url}" alt="{name}" style="max-width: 400px; border-radius: 8px; display: block; margin-bottom: 8px;" />')
|
||||
else:
|
||||
html_parts.append(f'<p><a href="{url}">{name}</a></p>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
html_parts.append('<h2>Troubleshooting Steps</h2>')
|
||||
|
||||
decisions = session.decisions
|
||||
@@ -541,10 +576,10 @@ def generate_html_export(session: Session, options: SessionExport, supporting_da
|
||||
return "\n".join(html_parts)
|
||||
|
||||
|
||||
def generate_psa_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str:
|
||||
def generate_psa_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None, uploads: list[dict] | None = None) -> str:
|
||||
"""Generate PSA/ticket note export optimized for ConnectWise and similar PSA tools."""
|
||||
if _is_procedural_session(session):
|
||||
return _generate_procedural_psa(session, options)
|
||||
return _generate_procedural_psa(session, options, uploads=uploads)
|
||||
lines = []
|
||||
outcome_label = _get_outcome_label(session)
|
||||
|
||||
@@ -661,6 +696,13 @@ def generate_psa_export(session: Session, options: SessionExport, supporting_dat
|
||||
scratchpad = getattr(session, 'scratchpad', '') or ''
|
||||
lines.append(scratchpad.strip() if scratchpad.strip() else "None")
|
||||
|
||||
# File upload evidence
|
||||
if uploads:
|
||||
lines.append("")
|
||||
lines.append("--- Evidence ---")
|
||||
for upload in uploads:
|
||||
lines.append(f"- {upload['filename']} — [{upload['url']}]")
|
||||
|
||||
# Branding footer
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
@@ -682,7 +724,7 @@ def _get_session_variables(session: Session) -> dict[str, str]:
|
||||
return {}
|
||||
|
||||
|
||||
def _generate_procedural_markdown(session: Session, options: SessionExport) -> str:
|
||||
def _generate_procedural_markdown(session: Session, options: SessionExport, uploads: list[dict] | None = None) -> str:
|
||||
"""Generate markdown export for procedural sessions."""
|
||||
lines = []
|
||||
outcome_label = _get_outcome_label(session)
|
||||
@@ -755,6 +797,21 @@ def _generate_procedural_markdown(session: Session, options: SessionExport) -> s
|
||||
lines.append(outcome_notes.strip())
|
||||
lines.append("")
|
||||
|
||||
# File upload evidence
|
||||
if uploads:
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append("## Evidence")
|
||||
lines.append("")
|
||||
for upload in uploads:
|
||||
name = upload["filename"]
|
||||
url = upload["url"]
|
||||
if upload.get("is_image"):
|
||||
lines.append(f"- ")
|
||||
else:
|
||||
lines.append(f"- [{name}]({url})")
|
||||
lines.append("")
|
||||
|
||||
# Branding footer
|
||||
lines.append("---")
|
||||
lines.append("Generated with ResolutionFlow — https://resolutionflow.com")
|
||||
@@ -762,7 +819,7 @@ def _generate_procedural_markdown(session: Session, options: SessionExport) -> s
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _generate_procedural_text(session: Session, options: SessionExport) -> str:
|
||||
def _generate_procedural_text(session: Session, options: SessionExport, uploads: list[dict] | None = None) -> str:
|
||||
"""Generate plain text export for procedural sessions."""
|
||||
lines = []
|
||||
outcome_label = _get_outcome_label(session)
|
||||
@@ -824,6 +881,13 @@ def _generate_procedural_text(session: Session, options: SessionExport) -> str:
|
||||
lines.append("-" * 20)
|
||||
lines.append(outcome_notes.strip())
|
||||
|
||||
# File upload evidence
|
||||
if uploads:
|
||||
lines.append("")
|
||||
lines.append("--- Evidence ---")
|
||||
for upload in uploads:
|
||||
lines.append(f"- {upload['filename']}: {upload['url']}")
|
||||
|
||||
# Branding footer
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
@@ -832,7 +896,7 @@ def _generate_procedural_text(session: Session, options: SessionExport) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _generate_procedural_html(session: Session, options: SessionExport) -> str:
|
||||
def _generate_procedural_html(session: Session, options: SessionExport, uploads: list[dict] | None = None) -> str:
|
||||
"""Generate HTML export for procedural sessions."""
|
||||
tree_name = html.escape(session.tree_snapshot.get("name", "Procedure"))
|
||||
outcome_label = _get_outcome_label(session)
|
||||
@@ -914,6 +978,19 @@ def _generate_procedural_html(session: Session, options: SessionExport) -> str:
|
||||
html_parts.append('<h2>Notes</h2>')
|
||||
html_parts.append(f'<div style="white-space: pre-wrap;">{html.escape(outcome_notes.strip())}</div>')
|
||||
|
||||
# File upload evidence
|
||||
if uploads:
|
||||
html_parts.append('<h3>Evidence</h3>')
|
||||
html_parts.append('<div class="evidence-grid" style="margin-bottom: 20px;">')
|
||||
for upload in uploads:
|
||||
name = html.escape(upload["filename"])
|
||||
url = html.escape(upload["url"])
|
||||
if upload.get("is_image"):
|
||||
html_parts.append(f'<img src="{url}" alt="{name}" style="max-width: 400px; border-radius: 8px; display: block; margin-bottom: 8px;" />')
|
||||
else:
|
||||
html_parts.append(f'<p><a href="{url}">{name}</a></p>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Branding footer
|
||||
html_parts.append('<hr style="margin-top: 32px; border: none; border-top: 1px solid #ddd;">')
|
||||
html_parts.append('<p style="margin-top: 12px; font-size: 0.8em; color: #999; text-align: center;">Generated with <a href="https://resolutionflow.com" style="color: #06b6d4; text-decoration: none;">ResolutionFlow</a> — https://resolutionflow.com</p>')
|
||||
@@ -922,7 +999,7 @@ def _generate_procedural_html(session: Session, options: SessionExport) -> str:
|
||||
return "\n".join(html_parts)
|
||||
|
||||
|
||||
def _generate_procedural_psa(session: Session, options: SessionExport) -> str:
|
||||
def _generate_procedural_psa(session: Session, options: SessionExport, uploads: list[dict] | None = None) -> str:
|
||||
"""Generate PSA/ticket export for procedural sessions."""
|
||||
lines = []
|
||||
outcome_label = _get_outcome_label(session)
|
||||
@@ -987,6 +1064,13 @@ def _generate_procedural_psa(session: Session, options: SessionExport) -> str:
|
||||
lines.append("--- TIME SPENT ---")
|
||||
lines.append(f"Duration: {_format_duration(session.started_at, session.completed_at)}")
|
||||
|
||||
# File upload evidence
|
||||
if uploads:
|
||||
lines.append("")
|
||||
lines.append("--- Evidence ---")
|
||||
for upload in uploads:
|
||||
lines.append(f"- {upload['filename']} — [{upload['url']}]")
|
||||
|
||||
# Branding footer
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
@@ -1095,6 +1179,32 @@ async def generate_pdf_export(session: Session, options: SessionExport, db) -> b
|
||||
for sd in supporting_data_rows
|
||||
]
|
||||
|
||||
# Query file upload evidence
|
||||
from app.models.file_upload import FileUpload
|
||||
from app.services import storage_service
|
||||
from app.core.config import settings as _settings
|
||||
uploads_for_export: list[dict] = []
|
||||
if _settings.STORAGE_ENDPOINT:
|
||||
try:
|
||||
uploads_result = await db.execute(
|
||||
sa_select(FileUpload)
|
||||
.where(FileUpload.session_id == session.id)
|
||||
.order_by(FileUpload.created_at)
|
||||
)
|
||||
upload_rows = uploads_result.scalars().all()
|
||||
for u in upload_rows:
|
||||
try:
|
||||
url = storage_service.get_presigned_url(u.storage_key)
|
||||
uploads_for_export.append({
|
||||
"filename": u.filename,
|
||||
"url": url,
|
||||
"is_image": u.content_type.startswith("image/"),
|
||||
})
|
||||
except Exception:
|
||||
pass # Skip individual uploads that fail URL generation
|
||||
except Exception:
|
||||
pass # Storage errors should not fail the export
|
||||
|
||||
# Calculate duration and format outcome
|
||||
duration = _format_duration(session.started_at, session.completed_at)
|
||||
session_date = session.started_at.strftime("%Y-%m-%d %H:%M")
|
||||
@@ -1172,6 +1282,7 @@ async def generate_pdf_export(session: Session, options: SessionExport, db) -> b
|
||||
summary=summary_text,
|
||||
steps=steps,
|
||||
supporting_data=supporting_data,
|
||||
uploads=uploads_for_export,
|
||||
generated_at=generated_at,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user