feat: AI chat conclusion + survey completion & management (#95)
* fix: increase assistant chat input height from 1 to 3 rows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Anthropic prompt caching to assistant chat Cache the static system prompt and conversation history prefix across turns, reducing input token costs by ~80% on multi-turn conversations. RAG context is intentionally uncached since it changes per query. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Microsoft Learn MCP integration + refine assistant system prompt - Integrate Microsoft Learn MCP server via Anthropic's MCP connector for real-time documentation lookups (docs search, fetch, code samples) - Refine system prompt: clear persona, structured answer guidelines, when to use RAG flows vs Microsoft Learn, guardrails against fabrication - Add ENABLE_MCP_MICROSOFT_LEARN config toggle (default: True) - Fix bugs from prior edit: wrong MCP URL, broken indentation, undefined usage/token variables, NOT_GIVEN for disabled MCP params - Log MCP tool usage and cache performance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: AI chat session conclusion + survey completion & management AI Assistant - Conclude Session: - 3-step modal: select outcome (resolved/escalated/paused), add notes, AI-generated summary - AI generates structured ticket notes from conversation transcript (PSA-ready format) - Copy to clipboard for pasting into ticketing systems - "Resume in New Chat" for paused sessions (pre-loads context into new chat) - Backend: POST /chats/{id}/conclude endpoint, conclusion_summary/outcome/concluded_at fields - Migration 048: add conclusion fields to assistant_chats Survey Completion Flow: - Email-to-self option after submission (branded HTML email with formatted responses) - Finish button navigates to /survey/thank-you page - Thank you page with close-window message and feedback email callout - Already-submitted state updated with same messaging - Backend: POST /survey/email-copy public endpoint Survey Admin Management: - Read/unread indicators (cyan dot, bold name, auto-mark on expand) - Unread count stat card - Per-row context menu: mark read/unread, archive/unarchive, delete - Bulk actions bar: select all, mark read/unread, archive, delete - Show Archived toggle to filter archived responses - Backend: 7 new admin endpoints (read, unread, archive, unarchive, delete, bulk) - Migration 049: add is_read, archived_at to survey_responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: initialize VerifyEmailPage state from token to avoid setState in effect Moves the no-token error case from useEffect into initial state to satisfy the react-hooks/set-state-in-effect ESLint rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #95.
This commit is contained in:
@@ -14,7 +14,7 @@ from app.core.email import EmailService
|
||||
from app.core.rate_limit import limiter
|
||||
from app.models.survey_invite import SurveyInvite
|
||||
from app.models.survey_response import SurveyResponse
|
||||
from app.schemas.survey import SurveyInviteStatus, SurveySubmission, SurveySubmissionResponse
|
||||
from app.schemas.survey import SurveyEmailCopyRequest, SurveyInviteStatus, SurveySubmission, SurveySubmissionResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -88,3 +88,79 @@ async def submit_survey(
|
||||
await db.commit()
|
||||
|
||||
return SurveySubmissionResponse(id=str(response.id))
|
||||
|
||||
|
||||
# Question metadata for formatting email copy
|
||||
_QUESTION_LABELS = {
|
||||
"prereqs": "Q1. Before you start troubleshooting, what info do you need?",
|
||||
"verify_fix": "Q2. After you apply a fix, how do you verify it actually worked?",
|
||||
"steps_at_a_time": "Q3. How many steps do you prefer to see at once?",
|
||||
"first_step": "Q4. \"Internet is down.\" What's your FIRST move?",
|
||||
"junior_mistake": "Q5. Most common mistake junior engineers make?",
|
||||
"pivot": "Q6. When do you stop pursuing one theory and pivot?",
|
||||
"scenario_approach": "Q7. First 3 diagnostic steps for this ticket.",
|
||||
"scenario_deeper": "Q8. Server pings fine, you can RDP in. What next?",
|
||||
"doc_pct": "Q9. Percentage of steps you actually document?",
|
||||
"go_to_commands": "Q10. Top 3 go-to PowerShell commands?",
|
||||
"secret_weapon": "Q11. Secret weapon command/tool/technique?",
|
||||
"gotcha": "Q12. Issue where the obvious diagnosis was WRONG?",
|
||||
"hard_rules": "Q13. Which rules do you follow?",
|
||||
"prioritization": "Q14. Rank factors by diagnostic priority.",
|
||||
"detail_level": "Q15. How specific should AI suggestions be?",
|
||||
"ai_personality": "Q16. What makes an AI feel like a useful colleague?",
|
||||
}
|
||||
|
||||
_QUESTION_ORDER = [
|
||||
"prereqs", "verify_fix", "steps_at_a_time", "first_step", "junior_mistake",
|
||||
"pivot", "scenario_approach", "scenario_deeper", "doc_pct", "go_to_commands",
|
||||
"secret_weapon", "gotcha", "hard_rules", "prioritization", "detail_level", "ai_personality",
|
||||
]
|
||||
|
||||
|
||||
@router.post("/survey/email-copy")
|
||||
@limiter.limit("5/hour")
|
||||
async def email_survey_copy(
|
||||
request: Request,
|
||||
data: SurveyEmailCopyRequest,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
):
|
||||
"""Email a copy of survey responses to the respondent."""
|
||||
from uuid import UUID as _UUID
|
||||
|
||||
try:
|
||||
resp_id = _UUID(data.response_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid response ID")
|
||||
|
||||
result = await db.execute(
|
||||
select(SurveyResponse).where(SurveyResponse.id == resp_id)
|
||||
)
|
||||
response = result.scalar_one_or_none()
|
||||
if not response:
|
||||
raise HTTPException(status_code=404, detail="Response not found")
|
||||
|
||||
# Build formatted responses for the email
|
||||
answers = response.responses or {}
|
||||
formatted_lines = []
|
||||
for qid in _QUESTION_ORDER:
|
||||
label = _QUESTION_LABELS.get(qid, qid)
|
||||
val = answers.get(qid)
|
||||
if isinstance(val, list):
|
||||
answer_str = ", ".join(str(v) for v in val)
|
||||
elif val is not None:
|
||||
answer_str = str(val)
|
||||
else:
|
||||
answer_str = "(no answer)"
|
||||
formatted_lines.append(f"{label}\n{answer_str}")
|
||||
|
||||
try:
|
||||
await EmailService.send_survey_copy_email(
|
||||
to_email=data.email,
|
||||
respondent_name=response.respondent_name,
|
||||
formatted_responses="\n\n".join(formatted_lines),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to send survey copy email to %s", data.email)
|
||||
raise HTTPException(status_code=502, detail="Failed to send email. Please try again.")
|
||||
|
||||
return {"message": "Email sent"}
|
||||
|
||||
Reference in New Issue
Block a user