_private/qwestly-docs/Features/qwestly-agent/agent-drafts-linkedin-suggestion.md
Table of Contents
Agent Drafts & LinkedIn Profile Suggestion
Overview
The qwestly-agent can generate LinkedIn About sections using the same LangSmith prompt as the candidate app's admin LinkedIn Suggestions page, persist results as drafts, and support in-place editing. A companion UI on the /agent page lets users browse, edit, and delete saved documents.
Architecture
User chat → agent suggests_linkedin_profile tool
→ fetches LinkedIn data from candidate /api/linkedin/profile
→ gathers context (interviews, KB entries)
→ calls api-python POST /api/prompts/invoke?output=json (sync)
→ extracts content from structured response
→ saves/updates user_documents draft
→ returns preview to user
Candidate /agent page → GET /api/agent/user-documents (proxy)
→ lists documents with content preview
→ click opens ProseMirror editor dialog
→ save via PATCH, delete via DELETE
Tools
suggest_linkedin_profile
File: tools/suggest_linkedin_profile.py
| Param | Type | Description |
|---|---|---|
max_length |
str? | Target word count |
additional_instructions |
str? | Extra prompt guidance |
document_id |
str? | If set, update existing draft instead of creating new |
Flow:
- Checks
candidates_enhanced.linkedin_user_name— returns error if missing - Fetches raw LinkedIn profile + interview/KB context
- Calls api-python
POST /api/prompts/invoke?output=json(sync) - Extracts
contentfrom structured{type: "json", json: {section, content}}blocks - If
document_idprovided: verifies ownership + type, updates draft. Otherwise: creates new draft. - Returns
{ok, draft_id, trace_id, preview}
Error cases:
| Error | Cause | Agent response |
|---|---|---|
no_linkedin_username |
No LinkedIn data | Ask user for username → call ingest_linkedin_profile |
document_not_found |
Invalid document_id | Tell user the draft was deleted |
wrong_document_type |
Editing non-linkedin-about doc | Tell user to use the right document |
prompt_invoke_failed |
api-python error | Tell user to retry |
Prompt config (generators/prompts.py):
| Field | Value |
|---|---|
| Name | linkedin-about-section |
| Commit | eb30bf89 |
| Model | deepseek:deepseek-v4-pro |
manage_documents
File: tools/manage_documents.py
get_my_documents(doc_type?, limit?)— List documents newest-first with truncated previews (200 chars). Omits full content.get_document(document_id)— Fetch full content by ID. Returns{ok: false}if not found.
Document storage
Collection: user_documents (agent-owned, same MongoDB as candidate_portal)
| Field | Type | Description |
|---|---|---|
id |
UUID string | Application-level ID |
user_id |
string | Auth0 user ID |
name |
string | Label (default "LinkedIn About Section") |
type |
string | Currently linkedin-about |
status |
string | draft or published |
content |
string | Plain text / markdown |
llm_trace_id |
string? | LangSmith run ID |
API: /api/user-documents (dual auth: JWT for candidate, X-API-Key for server-to-server)
| Method | Path | Auth |
|---|---|---|
| POST | /api/user-documents |
X-API-Key |
| GET | /api/user-documents |
JWT or X-API-Key |
| GET | /api/user-documents/{id} |
JWT or X-API-Key |
| PATCH | /api/user-documents/{id} |
JWT or X-API-Key |
| DELETE | /api/user-documents/{id} |
JWT or X-API-Key |
Candidate proxy: /api/agent/user-documents/[[...slug]] forwards to the agent API with JWT auth via agentFetch.
Test page (/test)
The agent's local dev test page (http://localhost:8003/test) has been extended:
- Sidebar tabs — Sessions / Documents
- Document list — type badge, status, date, delete button
- Document card — green card renders in chat when a tool result contains
draft_id, with preview and LangSmith trace link - Overlay viewer — click any document to view full content in a modal
- Delete — trash icon on each document item, with confirmation prompt
Candidate /agent page
The /agent page in the candidate app has been extended:
- Sidebar tabs — Sessions / Documents
- Document list — content preview (80 chars), type badge, status, date
- Delete dropdown — three-dot menu with Delete action
- ProseMirror editor dialog — full WYSIWYG editor with formatting toolbar (bold, italic, headings, lists, undo/redo). Uses
requestAnimationFramepolling to handle Radix Dialog's async portal rendering.
Key implementation notes
?output=jsonparameter — The prompt returns{section, content}. We pass?output=jsonto api-python which returns structured{type: "json", json: {...}}blocks. The tool extracts thecontentfield.document_idediting — Prevents duplicate drafts when users ask to refine existing content. Security: verifies document belongs to the user and type matches.- ProseMirror cleanup — The shared
ProseMirrorEditorcomponent has no useEffect cleanup, causing a double-render bug. The dialog usesProseMirrorViewdirectly with proper cleanup instead. - Dialog portal polling — Radix Dialog renders portal content asynchronously. A
requestAnimationFrameloop waits for the target DOM element before creating the editor.
Design plan
See _docs/agent-drafts-linkedin-design-plan.md (workspace root) for original design decisions, open questions, and architecture diagrams.