_private/qwestly-docs/Features/qwestly-agent/agent-drafts-linkedin-suggestion.md

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:

  1. Checks candidates_enhanced.linkedin_user_name — returns error if missing
  2. Fetches raw LinkedIn profile + interview/KB context
  3. Calls api-python POST /api/prompts/invoke?output=json (sync)
  4. Extracts content from structured {type: "json", json: {section, content}} blocks
  5. If document_id provided: verifies ownership + type, updates draft. Otherwise: creates new draft.
  6. 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 requestAnimationFrame polling to handle Radix Dialog's async portal rendering.

Key implementation notes

  1. ?output=json parameter — The prompt returns {section, content}. We pass ?output=json to api-python which returns structured {type: "json", json: {...}} blocks. The tool extracts the content field.
  2. document_id editing — Prevents duplicate drafts when users ask to refine existing content. Security: verifies document belongs to the user and type matches.
  3. ProseMirror cleanup — The shared ProseMirrorEditor component has no useEffect cleanup, causing a double-render bug. The dialog uses ProseMirrorView directly with proper cleanup instead.
  4. Dialog portal polling — Radix Dialog renders portal content asynchronously. A requestAnimationFrame loop 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.