feat: add Resolution and Next Steps sections to all export formats
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -175,6 +175,28 @@ def generate_markdown_export(session: Session, options: SessionExport) -> str:
|
|||||||
lines.append(f"*{decision['timestamp']}*")
|
lines.append(f"*{decision['timestamp']}*")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
|
# Resolution / Outcome Notes
|
||||||
|
_raw_notes = getattr(session, 'outcome_notes', None)
|
||||||
|
outcome_notes = _raw_notes if isinstance(_raw_notes, str) else ''
|
||||||
|
if outcome_notes.strip() and options.include_outcome_notes:
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Resolution")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(outcome_notes.strip())
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Next Steps
|
||||||
|
_raw_next = getattr(session, 'next_steps', None)
|
||||||
|
next_steps = _raw_next if isinstance(_raw_next, str) else ''
|
||||||
|
if next_steps.strip() and options.include_next_steps:
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Next Steps")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(next_steps.strip())
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -232,6 +254,24 @@ def generate_text_export(session: Session, options: SessionExport) -> str:
|
|||||||
if duration_seconds is not None:
|
if duration_seconds is not None:
|
||||||
lines.append(f" Duration: {_format_step_duration(duration_seconds)}")
|
lines.append(f" Duration: {_format_step_duration(duration_seconds)}")
|
||||||
|
|
||||||
|
# Resolution
|
||||||
|
_raw_notes = getattr(session, 'outcome_notes', None)
|
||||||
|
outcome_notes = _raw_notes if isinstance(_raw_notes, str) else ''
|
||||||
|
if outcome_notes.strip() and options.include_outcome_notes:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("RESOLUTION")
|
||||||
|
lines.append("-" * 20)
|
||||||
|
lines.append(outcome_notes.strip())
|
||||||
|
|
||||||
|
# Next Steps
|
||||||
|
_raw_next = getattr(session, 'next_steps', None)
|
||||||
|
next_steps = _raw_next if isinstance(_raw_next, str) else ''
|
||||||
|
if next_steps.strip() and options.include_next_steps:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("NEXT STEPS")
|
||||||
|
lines.append("-" * 20)
|
||||||
|
lines.append(next_steps.strip())
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -304,6 +344,20 @@ def generate_html_export(session: Session, options: SessionExport) -> str:
|
|||||||
html_parts.append(f'<p class="timestamp">{html.escape(str(decision["timestamp"]))}</p>')
|
html_parts.append(f'<p class="timestamp">{html.escape(str(decision["timestamp"]))}</p>')
|
||||||
html_parts.append('</div>')
|
html_parts.append('</div>')
|
||||||
|
|
||||||
|
# Resolution
|
||||||
|
_raw_notes = getattr(session, 'outcome_notes', None)
|
||||||
|
outcome_notes = _raw_notes if isinstance(_raw_notes, str) else ''
|
||||||
|
if outcome_notes.strip() and options.include_outcome_notes:
|
||||||
|
html_parts.append('<h2>Resolution</h2>')
|
||||||
|
html_parts.append(f'<div style="white-space: pre-wrap; margin-bottom: 20px;">{html.escape(outcome_notes.strip())}</div>')
|
||||||
|
|
||||||
|
# Next Steps
|
||||||
|
_raw_next = getattr(session, 'next_steps', None)
|
||||||
|
next_steps = _raw_next if isinstance(_raw_next, str) else ''
|
||||||
|
if next_steps.strip() and options.include_next_steps:
|
||||||
|
html_parts.append('<h2>Next Steps</h2>')
|
||||||
|
html_parts.append(f'<div style="white-space: pre-wrap; margin-bottom: 20px;">{html.escape(next_steps.strip())}</div>')
|
||||||
|
|
||||||
html_parts.extend(['</body>', '</html>'])
|
html_parts.extend(['</body>', '</html>'])
|
||||||
return "\n".join(html_parts)
|
return "\n".join(html_parts)
|
||||||
|
|
||||||
@@ -360,9 +414,13 @@ def generate_psa_export(session: Session, options: SessionExport) -> str:
|
|||||||
lines.append("No steps recorded.")
|
lines.append("No steps recorded.")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Resolution - last decision answer
|
# Resolution
|
||||||
lines.append("--- RESOLUTION ---")
|
lines.append("--- RESOLUTION ---")
|
||||||
if session.decisions:
|
_raw_notes = getattr(session, 'outcome_notes', None)
|
||||||
|
outcome_notes = _raw_notes if isinstance(_raw_notes, str) else ''
|
||||||
|
if outcome_notes.strip() and options.include_outcome_notes:
|
||||||
|
lines.append(outcome_notes.strip())
|
||||||
|
elif session.decisions:
|
||||||
last_decision = session.decisions[-1]
|
last_decision = session.decisions[-1]
|
||||||
resolution = last_decision.get("answer") or last_decision.get("question", "No resolution recorded")
|
resolution = last_decision.get("answer") or last_decision.get("question", "No resolution recorded")
|
||||||
lines.append(resolution)
|
lines.append(resolution)
|
||||||
@@ -372,6 +430,14 @@ def generate_psa_export(session: Session, options: SessionExport) -> str:
|
|||||||
lines.append(f"Outcome: {outcome_label}")
|
lines.append(f"Outcome: {outcome_label}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
|
# Next Steps
|
||||||
|
_raw_next = getattr(session, 'next_steps', None)
|
||||||
|
next_steps = _raw_next if isinstance(_raw_next, str) else ''
|
||||||
|
if next_steps.strip() and options.include_next_steps:
|
||||||
|
lines.append("--- NEXT STEPS ---")
|
||||||
|
lines.append(next_steps.strip())
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
# Time spent
|
# Time spent
|
||||||
lines.append("--- TIME SPENT ---")
|
lines.append("--- TIME SPENT ---")
|
||||||
duration = _format_duration(session.started_at, session.completed_at)
|
duration = _format_duration(session.started_at, session.completed_at)
|
||||||
|
|||||||
@@ -1058,3 +1058,129 @@ class TestSessions:
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["next_steps"] == "Schedule follow-up call"
|
assert response.json()["next_steps"] == "Schedule follow-up call"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_includes_outcome_notes_in_resolution(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test that outcome_notes appear as Resolution section in exports."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/complete",
|
||||||
|
json={
|
||||||
|
"outcome": "resolved",
|
||||||
|
"outcome_notes": "Replaced failed DIMM in slot A2",
|
||||||
|
"next_steps": "Monitor for 24 hours"
|
||||||
|
},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test markdown
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.text
|
||||||
|
assert "## Resolution" in content
|
||||||
|
assert "Replaced failed DIMM in slot A2" in content
|
||||||
|
assert "## Next Steps" in content
|
||||||
|
assert "Monitor for 24 hours" in content
|
||||||
|
|
||||||
|
# Test text
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "text"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "RESOLUTION" in content
|
||||||
|
assert "Replaced failed DIMM in slot A2" in content
|
||||||
|
assert "NEXT STEPS" in content
|
||||||
|
assert "Monitor for 24 hours" in content
|
||||||
|
|
||||||
|
# Test HTML
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "html"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "Resolution" in content
|
||||||
|
assert "Replaced failed DIMM in slot A2" in content
|
||||||
|
assert "Next Steps" in content
|
||||||
|
assert "Monitor for 24 hours" in content
|
||||||
|
|
||||||
|
# Test PSA
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "psa"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "Replaced failed DIMM in slot A2" in content
|
||||||
|
assert "Monitor for 24 hours" in content
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_omits_empty_resolution_and_next_steps(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test that empty outcome_notes/next_steps don't create empty sections."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/complete",
|
||||||
|
json={"outcome": "resolved"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown"},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "## Resolution" not in content
|
||||||
|
assert "## Next Steps" not in content
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_exclude_outcome_notes_flag(
|
||||||
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||||
|
):
|
||||||
|
"""Test include_outcome_notes=False suppresses resolution section."""
|
||||||
|
create_response = await client.post(
|
||||||
|
"/api/v1/sessions",
|
||||||
|
json={"tree_id": test_tree["id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
session_id = create_response.json()["id"]
|
||||||
|
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/complete",
|
||||||
|
json={
|
||||||
|
"outcome": "resolved",
|
||||||
|
"outcome_notes": "Should not appear"
|
||||||
|
},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/v1/sessions/{session_id}/export",
|
||||||
|
json={"format": "markdown", "include_outcome_notes": False},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
content = response.text
|
||||||
|
assert "## Resolution" not in content
|
||||||
|
assert "Should not appear" not in content
|
||||||
|
|||||||
Reference in New Issue
Block a user