diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/.env.example b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/.env.example new file mode 100644 index 00000000..43c1b6f4 --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/.env.example @@ -0,0 +1 @@ +SUPERDOCS_API_KEY=your_superdocs_api_key_here diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/.gitignore b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/.gitignore new file mode 100644 index 00000000..c4720b1c --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/.gitignore @@ -0,0 +1,6 @@ +.env +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ +outputs/* \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/README.md b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/README.md new file mode 100644 index 00000000..cbb6bbca --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/README.md @@ -0,0 +1,359 @@ +# Evidence-First Resume Bullet Rewriter + +Built by **Kalpesh Sharma** for the **SuperDocs Engineer Task**. + +An evidence-first resume editing workflow that strengthens resume bullets without inventing metrics, responsibilities, technologies, or experience. + +The core rule is simple: + +> If measurable impact is missing, ask for it instead of making it up. + +![Evidence-First Resume Bullet Rewriter](assets/evidence_first_review.png) + +## What it does + +The app takes a resume bullet and checks whether the information needed for a stronger outcome-oriented statement is actually supported. + +It can: + +- detect explicit quantitative evidence already present in a bullet +- preserve existing metrics +- ask the user for measurable impact when none is available +- reject unsupported claims such as fabricated leadership experience +- generate a conservative rewrite using only supplied evidence +- upload the source resume to SuperDocs +- show the proposed document edit before applying it +- require explicit human approval or rejection +- export the reviewed DOCX after approval + +## Example + +### Source + +```text +Automated invoice processing using Python. +``` + +The source contains no measurable result, so the app does **not** invent one. + +Instead it asks for evidence such as: + +```text +Reduced processing time from 3 hours to 45 minutes. +``` + +The resulting proposal becomes: + +```text +Automated invoice processing using Python, reducing processing time from 3 hours to 45 minutes. +``` + +The values `3 hours` and `45 minutes` came directly from the user. + +## Unsupported-claim protection + +If the source says: + +```text +Built Power BI dashboards for operational reporting. +``` + +and the user requests: + +```text +Make this say I led a five-person analytics team. +``` + +the request is rejected because the source contains no evidence of leadership responsibility. + +The app does not silently add the claim. + +## Workflow + +```text +Resume bullet + ↓ +Evidence detection + ↓ +Is measurable impact available? + │ + ├── No → ask the user + │ + └── Yes + ↓ +Supported-claim validation + ↓ +Evidence-grounded rewrite + ↓ +Upload source DOCX to SuperDocs + ↓ +SuperDocs proposed edit + ↓ +Human review + ↙ ↘ + Reject Approve + ↓ + Reviewed DOCX + ↓ + Export +``` + +## SuperDocs integration + +SuperDocs is used for the actual document-editing workflow. + +The app: + +1. uploads the resume DOCX into a SuperDocs session +2. sends a surgical edit instruction containing the exact source and replacement bullet +3. starts an asynchronous review with `ask_every_time` +4. polls until the job reaches an actionable state +5. parses the proposed changes +6. displays the before/after diff +7. sends the human approval or rejection decision +8. exports the reviewed DOCX + +The edit instruction explicitly tells SuperDocs not to modify unrelated resume content or introduce new claims. + +## Human review + +Nothing is applied automatically. + +The user sees the proposed SuperDocs change first and can choose: + +- **Approve proposed change** +- **Reject proposed change** + +Only an approved edit is allowed to become part of the reviewed document. + +## Graceful re-entry + +SuperDocs review jobs can take time. + +If a review has already reached `awaiting_approval`, the app reuses that existing job instead of starting another review request. + +This avoids unnecessary duplicate operations and makes the workflow safe to resume. + +## Pending-change parsing + +SuperDocs may return proposed changes as either: + +- a list +- a JSON-encoded string + +and current review jobs expose them through job metadata. + +The parser handles these forms explicitly and fails loudly if malformed content is returned instead of silently treating it as an empty successful diff. + +## Proof cases + +### Case 1 — missing metric + +Source: + +```text +Automated invoice processing using Python. +``` + +Result: + +```text +More evidence needed +``` + +No metric is invented. + +After the user provides: + +```text +Reduced processing time from 3 hours to 45 minutes. +``` + +the values are used in the proposed rewrite. + +### Case 2 — metric already exists + +Source: + +```text +Processed 500 invoices per month using Python. +``` + +Result: + +```text +Ready for review +``` + +`500 invoices` is preserved exactly. + +### Case 3 — unsupported experience + +Source: + +```text +Built Power BI dashboards for operational reporting. +``` + +Requested claim: + +```text +Make this say I led a five-person analytics team. +``` + +Result: + +```text +Unsupported request +``` + +No leadership claim is added. + +## Verified SuperDocs flow + +The complete workflow was tested with a synthetic resume. + +```text +Evidence detection PASS +Missing-metric gate PASS +Unsupported-claim gate PASS +SuperDocs upload PASS +Proposed edit PASS +Human approval PASS +Approved edit persisted PASS +DOCX export PASS +Unrelated bullets preserved PASS +``` + +The final exported resume changed only: + +```text +Automated invoice processing using Python. +``` + +to: + +```text +Automated invoice processing using Python, reducing processing time from 3 hours to 45 minutes. +``` + +The remaining resume bullets were unchanged. + +## Project structure + +```text +impact-quantifying-bullet-rewriter/ +│ +├── app.py +├── README.md +├── requirements.txt +├── .env.example +├── .gitignore +│ +├── assets/ +│ └── evidence_first_review.png +│ +├── core/ +│ ├── __init__.py +│ ├── evidence.py +│ ├── models.py +│ └── rewriter.py +│ +├── sample_documents/ +│ ├── generate_sample_resume.py +│ └── sample_resume.docx +│ +├── superdocs/ +│ ├── __init__.py +│ ├── client.py +│ ├── parser.py +│ └── polling.py +│ +└── tests/ + ├── test_evidence.py + ├── test_rewriter.py + └── test_superdocs_parser.py +``` + +## Setup + +Create a virtual environment if needed: + +```bash +python -m venv .venv +``` + +Activate it and install dependencies: + +```bash +pip install -r requirements.txt +``` + +Create a local `.env`: + +```text +SUPERDOCS_API_KEY=your_superdocs_api_key +``` + +Never commit the real `.env` file. + +## Generate the sample resume + +```bash +python sample_documents/generate_sample_resume.py +``` + +## Run tests + +```bash +python -m pytest -v +``` + +The test suite does not require a live SuperDocs API key. + +## Run the app + +```bash +streamlit run app.py +``` + +Then: + +1. analyse a resume bullet +2. provide missing evidence if requested +3. upload `sample_documents/sample_resume.docx` +4. open it in SuperDocs +5. start Review +6. inspect the proposed before/after change +7. approve or reject it +8. export the reviewed DOCX + +## Design principles + +### Evidence over invention + +A stronger-looking resume is not useful if its claims are false. + +Metrics are only used when they already exist in the source or are explicitly supplied by the user. + +### Surgical edits + +SuperDocs receives an exact source bullet and exact replacement bullet. + +The instruction explicitly prohibits unrelated edits. + +### Human accountability + +The system can propose a change. + +The human decides whether the document actually changes. + +### Graceful failure + +Missing evidence, unsupported claims, malformed pending changes, missing API configuration, failed jobs, and timeouts are surfaced rather than hidden. + +--- + +The goal of this build is not to make every resume bullet sound impressive. + +It is to make supported experience clearer **without crossing the line from rewriting into fabrication**. diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/app.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/app.py new file mode 100644 index 00000000..b44382a1 --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/app.py @@ -0,0 +1,1164 @@ +from __future__ import annotations + +import tempfile +import time +from pathlib import Path +from uuid import uuid4 + +import streamlit as st + +from core.rewriter import propose_rewrite +from superdocs.client import ( + SuperDocsClient, + SuperDocsError, +) +from superdocs.parser import ( + SuperDocsParseError, + pending_changes_from_job, +) +from superdocs.polling import ( + wait_until_action_needed, +) + + +st.set_page_config( + page_title="Evidence-First Resume Bullet Rewriter", + page_icon="📝", + layout="wide", +) + + +# ========================================================== +# STATE +# ========================================================== + + +def initialize_state() -> None: + defaults = { + "proposal": None, + "source_bullet": None, + "requested_claim": "", + "resume_session_id": None, + "resume_document_id": None, + "resume_filename": None, + "review_job_id": None, + "pending_changes": [], + "review_status": None, + "review_decision": None, + "reviewed_docx_bytes": None, + } + + for key, value in defaults.items(): + if key not in st.session_state: + st.session_state[key] = value + + +def reset_review_state() -> None: + st.session_state.review_job_id = None + st.session_state.pending_changes = [] + st.session_state.review_status = None + st.session_state.review_decision = None + st.session_state.reviewed_docx_bytes = None + + +initialize_state() + + +# ========================================================== +# SUPERDOCS HELPERS +# ========================================================== + + +def upload_resume_to_superdocs( + uploaded_file, +) -> None: + suffix = ( + Path(uploaded_file.name).suffix + or ".docx" + ) + + temp_path: Path | None = None + + try: + with tempfile.NamedTemporaryFile( + delete=False, + suffix=suffix, + ) as temp_file: + temp_file.write( + uploaded_file.getvalue() + ) + + temp_path = Path( + temp_file.name + ) + + client = SuperDocsClient() + + requested_session_id = ( + "resume-bullet-" + + uuid4().hex[:10] + ) + + upload = client.upload_document( + path=temp_path, + session_id=requested_session_id, + open_mode="replace", + ) + + st.session_state.resume_session_id = ( + upload.get("session_id") + or requested_session_id + ) + + st.session_state.resume_document_id = ( + upload.get("document_id") + ) + + st.session_state.resume_filename = ( + uploaded_file.name + ) + + reset_review_state() + + finally: + if temp_path is not None: + temp_path.unlink( + missing_ok=True + ) + + +def wait_until_completed( + *, + client: SuperDocsClient, + job_id: str, + timeout_seconds: int = 180, + poll_interval: float = 2.0, +) -> dict: + """ + Used after the human approval decision. + + Unlike wait_until_action_needed(), this keeps + polling past awaiting_approval until the job + actually completes. + """ + + started = time.monotonic() + + while True: + job = client.get_job( + job_id=job_id + ) + + status = job.get("status") + + if status == "completed": + return job + + if status == "failed": + raise SuperDocsError( + job.get( + "error", + "SuperDocs job failed.", + ) + ) + + if status == "cancelled": + raise SuperDocsError( + "SuperDocs job was cancelled." + ) + + elapsed = ( + time.monotonic() + - started + ) + + if elapsed >= timeout_seconds: + raise TimeoutError( + f"Job {job_id} did not complete " + f"within {timeout_seconds} seconds." + ) + + time.sleep( + poll_interval + ) + + +def build_review_instruction( + original: str, + proposed: str, +) -> str: + return f""" +Replace exactly this resume bullet: + +SOURCE BULLET: +{original} + +REPLACEMENT BULLET: +{proposed} + +Rules: +- Change only the source bullet shown above. +- Use the replacement wording exactly as supplied. +- Do not modify any other resume content. +- Do not add new metrics. +- Do not change existing metrics. +- Do not invent responsibilities. +- Do not invent leadership experience. +- Do not invent technologies. +- Do not make any other edits. +- If the exact source bullet cannot be found, do not substitute a different bullet. +""".strip() + + +def export_reviewed_resume() -> bytes: + session_id = ( + st.session_state.resume_session_id + ) + + if not session_id: + raise RuntimeError( + "No SuperDocs resume session exists." + ) + + source_name = ( + st.session_state.resume_filename + or "resume.docx" + ) + + output_name = ( + Path(source_name).stem + + "_reviewed.docx" + ) + + temp_path: Path | None = None + + try: + with tempfile.NamedTemporaryFile( + delete=False, + suffix=".docx", + ) as temp_file: + temp_path = Path( + temp_file.name + ) + + client = SuperDocsClient() + + client.export_document( + session_id=session_id, + output_path=temp_path, + filename=output_name, + file_format="docx", + ) + + return temp_path.read_bytes() + + finally: + if temp_path is not None: + temp_path.unlink( + missing_ok=True + ) + + +# ========================================================== +# HEADER +# ========================================================== + + +st.title( + "Evidence-First Resume Bullet Rewriter" +) + +st.caption( + "Strengthen resume bullets without inventing " + "metrics, experience, or responsibilities." +) + +st.info( + "This tool only uses evidence supplied in the " + "resume or by the user. If measurable impact " + "is missing, it asks instead of guessing." +) + + +# ========================================================== +# 1. SOURCE BULLET +# ========================================================== + + +st.subheader( + "1. Source bullet" +) + +source_bullet = st.text_area( + "Paste one resume bullet", + value=( + "Automated invoice processing " + "using Python." + ), + height=100, +) + + +# ========================================================== +# 2. OPTIONAL REQUEST +# ========================================================== + + +st.subheader( + "2. Optional instruction" +) + +requested_claim = st.text_input( + "Anything specific you want emphasized?", + placeholder=( + "Example: Emphasize my " + "leadership experience" + ), +) + + +if st.button( + "Analyse bullet", + type="primary", + use_container_width=True, +): + proposal = propose_rewrite( + source_bullet, + requested_claim=( + requested_claim + or None + ), + ) + + st.session_state.proposal = ( + proposal + ) + + st.session_state.source_bullet = ( + source_bullet + ) + + st.session_state.requested_claim = ( + requested_claim + ) + + reset_review_state() + + +proposal = ( + st.session_state.proposal +) + + +# ========================================================== +# 3. EVIDENCE CHECK +# ========================================================== + + +if proposal: + st.divider() + + st.subheader( + "3. Evidence check" + ) + + left, right = st.columns(2) + + with left: + st.markdown( + "**Source bullet**" + ) + + st.write( + proposal.original_bullet + ) + + with right: + st.markdown( + "**Status**" + ) + + if proposal.status == "ready": + st.success( + "Ready for review" + ) + + elif ( + proposal.status + == "needs_metric" + ): + st.warning( + "More evidence needed" + ) + + else: + st.error( + "Unsupported request" + ) + + if proposal.evidence_used: + st.markdown( + "**Evidence found**" + ) + + for evidence in ( + proposal.evidence_used + ): + st.code( + evidence + ) + + else: + st.caption( + "No explicit quantitative " + "evidence found." + ) + + + # ====================================================== + # MISSING METRIC + # ====================================================== + + if ( + proposal.status + == "needs_metric" + ): + st.markdown( + "### Missing evidence" + ) + + st.write( + proposal.reason + ) + + for question in ( + proposal.questions + ): + st.info( + question + ) + + supplied_metric = st.text_input( + "Add measurable evidence " + "you actually know", + placeholder=( + "Example: Reduced processing " + "time from 3 hours to " + "45 minutes" + ), + key="supplied_metric", + ) + + if st.button( + "Use this evidence", + use_container_width=True, + ): + updated = propose_rewrite( + st.session_state[ + "source_bullet" + ], + supplied_metric=( + supplied_metric + ), + requested_claim=( + st.session_state[ + "requested_claim" + ] + or None + ), + ) + + st.session_state.proposal = ( + updated + ) + + reset_review_state() + + st.rerun() + + + # ====================================================== + # UNSUPPORTED REQUEST + # ====================================================== + + elif ( + proposal.status + == "unsupported" + ): + st.error( + proposal.reason + ) + + st.markdown( + """ +The requested claim was **not added** +because it could not be supported by +the supplied resume evidence. +""" + ) + + + # ====================================================== + # READY + # ====================================================== + + elif ( + proposal.status + == "ready" + ): + st.subheader( + "4. Proposed rewrite" + ) + + before, after = ( + st.columns(2) + ) + + with before: + st.markdown( + "**Before**" + ) + + st.text_area( + "Original", + proposal.original_bullet, + height=120, + disabled=True, + label_visibility="collapsed", + key="before_text", + ) + + with after: + st.markdown( + "**After**" + ) + + st.text_area( + "Proposal", + ( + proposal.proposed_bullet + or "" + ), + height=120, + disabled=True, + label_visibility="collapsed", + key="after_text", + ) + + st.success( + "No unsupported metric or " + "experience was introduced." + ) + + st.caption( + proposal.reason + ) + + + # ================================================== + # 5. UPLOAD RESUME + # ================================================== + + st.divider() + + st.subheader( + "5. Open resume in SuperDocs" + ) + + st.write( + "Upload the DOCX that contains " + "the source bullet. The proposed " + "rewrite will not be applied until " + "you explicitly approve it." + ) + + uploaded_resume = ( + st.file_uploader( + "Upload resume DOCX", + type=["docx"], + key="resume_uploader", + ) + ) + + if ( + uploaded_resume + is not None + ): + st.caption( + "Selected: " + f"`{uploaded_resume.name}`" + ) + + if st.button( + "Open resume in SuperDocs", + type="primary", + key="upload_resume_button", + ): + try: + with st.spinner( + "Uploading resume " + "to SuperDocs..." + ): + upload_resume_to_superdocs( + uploaded_resume + ) + + st.success( + "Resume opened " + "successfully." + ) + + st.rerun() + + except Exception as exc: + st.error( + str(exc) + ) + + + # ================================================== + # 6. HUMAN REVIEW + # ================================================== + + if ( + st.session_state + .resume_session_id + ): + st.success( + "Resume is ready " + "in SuperDocs." + ) + + st.caption( + "Document: " + + str( + st.session_state + .resume_filename + ) + ) + + st.subheader( + "6. SuperDocs human review" + ) + + review_instruction = ( + build_review_instruction( + proposal.original_bullet, + ( + proposal + .proposed_bullet + or "" + ), + ) + ) + + with st.expander( + "Exact edit instruction" + ): + st.code( + review_instruction + ) + + if ( + not st.session_state + .pending_changes + and st.session_state + .review_status + not in { + "completed", + "rejected", + } + ): + button_label = ( + "Resume SuperDocs Review" + if ( + st.session_state.review_job_id + and st.session_state.review_status + == "awaiting_approval" + ) + else "Start SuperDocs Review" + ) + + if st.button( + button_label, + type="primary", + key="start_review_button", + ): + try: + client = SuperDocsClient() + + existing_job_id = ( + st.session_state + .review_job_id + ) + + with st.spinner( + "Loading the existing " + "SuperDocs review..." + if ( + existing_job_id + and st.session_state + .review_status + == "awaiting_approval" + ) + else ( + "SuperDocs is preparing " + "the proposed edit..." + ) + ): + if ( + existing_job_id + and st.session_state + .review_status + == "awaiting_approval" + ): + # Graceful re-entry: + # reuse the already-created job instead + # of starting another review request. + job_id = existing_job_id + job = client.get_job( + job_id=job_id + ) + + else: + started = ( + client.start_async_chat( + session_id=( + st.session_state + .resume_session_id + ), + message=( + review_instruction + ), + approval_mode=( + "ask_every_time" + ), + ) + ) + + job_id = started.get( + "job_id" + ) + + if not job_id: + raise RuntimeError( + "SuperDocs did not " + "return a job_id." + ) + + st.session_state[ + "review_job_id" + ] = job_id + + job = wait_until_action_needed( + client=client, + job_id=job_id, + timeout_seconds=180, + ) + + status = job.get( + "status" + ) + + st.session_state[ + "review_status" + ] = status + + metadata = ( + job.get("metadata") + or {} + ) + + if ( + status + == "awaiting_approval" + and isinstance( + metadata, + dict, + ) + and metadata.get( + "awaiting_kind" + ) + == "continue_prompt" + ): + raise RuntimeError( + "SuperDocs requested " + "continuation for a larger " + "operation. This demo expects " + "one small surgical edit." + ) + + changes = ( + pending_changes_from_job( + job + ) + ) + + st.session_state[ + "pending_changes" + ] = changes + + if ( + status + == "awaiting_approval" + and not changes + ): + raise RuntimeError( + "SuperDocs reached " + "awaiting_approval but " + "returned no proposed changes." + ) + + st.rerun() + + except ( + SuperDocsError, + SuperDocsParseError, + TimeoutError, + RuntimeError, + ) as exc: + st.error( + str(exc) + ) + + + # ============================================== + # SHOW PROPOSED CHANGES + # ============================================== + + pending_changes = ( + st.session_state + .pending_changes + ) + + if pending_changes: + st.warning( + "SuperDocs has proposed " + "a change. Nothing has " + "been applied yet." + ) + + for index, change in enumerate( + pending_changes, + start=1, + ): + with st.container( + border=True + ): + st.markdown( + f"### Proposed " + f"change {index}" + ) + + operation = ( + change.get( + "operation" + ) + ) + + explanation = ( + change.get( + "ai_explanation" + ) + ) + + if operation: + st.caption( + "Operation: " + + str( + operation + ) + ) + + if explanation: + st.write( + "**Explanation:** " + + str( + explanation + ) + ) + + before_html = ( + change.get( + "old_html" + ) + or change.get( + "before_html" + ) + or change.get( + "before" + ) + or "" + ) + + after_html = ( + change.get( + "new_html" + ) + or change.get( + "after_html" + ) + or change.get( + "after" + ) + or "" + ) + + left_change, right_change = ( + st.columns(2) + ) + + with left_change: + st.markdown( + "**Before**" + ) + + st.code( + before_html + or "(empty)", + language="html", + ) + + with right_change: + st.markdown( + "**After**" + ) + + st.code( + after_html + or "(empty)", + language="html", + ) + + approve_col, reject_col = ( + st.columns(2) + ) + + with approve_col: + if st.button( + "Approve proposed change", + type="primary", + use_container_width=True, + key="approve_change", + ): + try: + client = ( + SuperDocsClient() + ) + + with st.spinner( + "Applying approved " + "change..." + ): + decisions = [ + { + "change_id": change["change_id"], + "approved": True, + } + for change in pending_changes + ] + + client.approve_changes( + session_id=( + st.session_state + .resume_session_id + ), + job_id=( + st.session_state + .review_job_id + ), + changes=decisions, + approved=True, + ) + + wait_until_completed( + client=client, + job_id=( + st.session_state + .review_job_id + ), + timeout_seconds=180, + ) + + st.session_state[ + "pending_changes" + ] = [] + + st.session_state[ + "review_status" + ] = "completed" + + st.session_state[ + "review_decision" + ] = "approved" + + st.success( + "Change approved " + "and applied." + ) + + st.rerun() + + except Exception as exc: + st.error( + str(exc) + ) + + with reject_col: + if st.button( + "Reject proposed change", + use_container_width=True, + key="reject_change", + ): + try: + client = ( + SuperDocsClient() + ) + + with st.spinner( + "Rejecting proposed " + "change..." + ): + decisions = [ + { + "change_id": change["change_id"], + "approved": False, + } + for change in pending_changes + ] + + client.approve_changes( + session_id=( + st.session_state + .resume_session_id + ), + job_id=( + st.session_state + .review_job_id + ), + changes=decisions, + approved=False, + ) + + st.session_state[ + "pending_changes" + ] = [] + + st.session_state[ + "review_status" + ] = "rejected" + + st.session_state[ + "review_decision" + ] = "rejected" + + st.rerun() + + except Exception as exc: + st.error( + str(exc) + ) + + + # ============================================== + # APPROVED + # ============================================== + + if ( + st.session_state + .review_decision + == "approved" + ): + st.success( + "Human approval complete. " + "The reviewed document " + "can now be exported." + ) + + st.subheader( + "7. Export reviewed DOCX" + ) + + if st.button( + "Prepare reviewed DOCX", + type="primary", + key="prepare_export", + ): + try: + with st.spinner( + "Exporting reviewed " + "resume..." + ): + ( + st.session_state[ + "reviewed_docx_bytes" + ] + ) = ( + export_reviewed_resume() + ) + + st.rerun() + + except Exception as exc: + st.error( + str(exc) + ) + + if ( + st.session_state + .reviewed_docx_bytes + ): + source_name = ( + st.session_state + .resume_filename + or "resume.docx" + ) + + output_name = ( + Path(source_name).stem + + "_reviewed.docx" + ) + + st.download_button( + "Download reviewed DOCX", + data=( + st.session_state + .reviewed_docx_bytes + ), + file_name=( + output_name + ), + mime=( + "application/vnd." + "openxmlformats-" + "officedocument." + "wordprocessingml." + "document" + ), + use_container_width=True, + key="download_reviewed_docx", + ) + + + # ============================================== + # REJECTED + # ============================================== + + elif ( + st.session_state + .review_decision + == "rejected" + ): + st.warning( + "The proposed change was " + "rejected. The resume was " + "not intentionally changed " + "by this review." + ) + + if st.button( + "Reset review", + key="reset_review", + ): + reset_review_state() + st.rerun() \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/assets/evidence_first_review.png b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/assets/evidence_first_review.png new file mode 100644 index 00000000..dcf9493c Binary files /dev/null and b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/assets/evidence_first_review.png differ diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/__init__.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/evidence.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/evidence.py new file mode 100644 index 00000000..356610eb --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/evidence.py @@ -0,0 +1,107 @@ +import re + +from core.models import BulletAnalysis, EvidenceItem + + +PATTERNS: list[tuple[str, re.Pattern[str]]] = [ + ( + "percentage", + re.compile(r"\b\d+(?:\.\d+)?\s*%"), + ), + ( + "currency", + re.compile( + r"(?:₹|\$|€|£)\s?\d[\d,]*(?:\.\d+)?" + r"|\b\d[\d,]*(?:\.\d+)?\s?(?:USD|INR|EUR|GBP)\b", + re.IGNORECASE, + ), + ), + ( + "duration", + re.compile( + r"\b\d+(?:\.\d+)?\s*" + r"(?:seconds?|secs?|minutes?|mins?|hours?|hrs?|days?|weeks?|months?)\b", + re.IGNORECASE, + ), + ), + ( + "volume", + re.compile( + r"\b\d[\d,]*(?:\.\d+)?\s+" + r"(?:" + r"invoices?|records?|reports?|files?|documents?|tickets?|" + r"customers?|clients?|users?|transactions?|orders?|requests?|" + r"dashboards?|pipelines?|models?|queries?|jobs?|cases?" + r")\b", + re.IGNORECASE, + ), + ), +] + + +def _overlaps( + start: int, + end: int, + occupied: list[tuple[int, int]], +) -> bool: + return any(start < existing_end and end > existing_start for existing_start, existing_end in occupied) + + +def extract_evidence(text: str) -> list[EvidenceItem]: + """ + Extract explicit quantitative evidence from a resume bullet. + + Important: + This function only detects evidence that is literally present + in the supplied text. It never infers or invents metrics. + """ + if not text or not text.strip(): + return [] + + evidence: list[EvidenceItem] = [] + occupied: list[tuple[int, int]] = [] + + for kind, pattern in PATTERNS: + for match in pattern.finditer(text): + start, end = match.span() + + if _overlaps(start, end, occupied): + continue + + evidence.append( + EvidenceItem( + text=match.group(0), + kind=kind, # type: ignore[arg-type] + start=start, + end=end, + ) + ) + occupied.append((start, end)) + + evidence.sort(key=lambda item: item.start) + return evidence + + +def analyze_bullet(text: str) -> BulletAnalysis: + cleaned = text.strip() + evidence = extract_evidence(cleaned) + + missing: list[str] = [] + + if not evidence: + missing.append("quantified impact") + + return BulletAnalysis( + original_bullet=cleaned, + evidence=evidence, + has_quantified_evidence=bool(evidence), + missing_evidence=missing, + ) + + +def metric_question() -> str: + return ( + "Do you know a measurable result for this work, such as time saved, " + "processing-time reduction, volume handled, error reduction, " + "revenue generated, or cost saved?" + ) \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/models.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/models.py new file mode 100644 index 00000000..20d53cd1 --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/models.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass, field +from typing import Literal + + +EvidenceKind = Literal[ + "percentage", + "currency", + "duration", + "volume", + "number", +] + + +@dataclass(frozen=True) +class EvidenceItem: + text: str + kind: EvidenceKind + start: int + end: int + + +@dataclass +class BulletAnalysis: + original_bullet: str + evidence: list[EvidenceItem] = field(default_factory=list) + has_quantified_evidence: bool = False + missing_evidence: list[str] = field(default_factory=list) + + +RewriteStatus = Literal[ + "ready", + "needs_metric", + "unsupported", +] + + +@dataclass +class RewriteProposal: + original_bullet: str + proposed_bullet: str | None + status: RewriteStatus + reason: str + evidence_used: list[str] = field(default_factory=list) + questions: list[str] = field(default_factory=list) \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/rewriter.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/rewriter.py new file mode 100644 index 00000000..907b58f1 --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/core/rewriter.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +import re + +from core.evidence import analyze_bullet, extract_evidence, metric_question +from core.models import RewriteProposal + + +LEADERSHIP_REQUEST_TERMS = ( + "led", + "lead ", + "managed", + "supervised", + "mentored", + "team lead", + "leadership", +) + +LEADERSHIP_SOURCE_TERMS = ( + "led", + "managed", + "supervised", + "mentored", + "team lead", + "direct reports", +) + + +def _clean(text: str) -> str: + return " ".join(text.strip().split()) + + +def _ensure_period(text: str) -> str: + text = text.strip() + + if not text: + return text + + if text[-1] not in ".!?": + return text + "." + + return text + + +def _contains_any(text: str, terms: tuple[str, ...]) -> bool: + lowered = text.lower() + return any(term in lowered for term in terms) + + +def request_is_supported( + source_bullet: str, + requested_claim: str, +) -> tuple[bool, str]: + """ + Check whether a requested claim is supported by the source bullet. + + This deliberately handles only claim families we can validate + deterministically. Unsupported high-risk additions are rejected + rather than guessed. + """ + source = _clean(source_bullet) + request = _clean(requested_claim) + + if not request: + return True, "No additional claim requested." + + requests_leadership = _contains_any( + request, + LEADERSHIP_REQUEST_TERMS, + ) + + if requests_leadership: + source_supports_leadership = _contains_any( + source, + LEADERSHIP_SOURCE_TERMS, + ) + + if not source_supports_leadership: + return ( + False, + "The requested leadership claim is not supported by " + "the supplied resume evidence.", + ) + + return True, "Requested claim is supported by the supplied evidence." + + +def strengthen_structure(bullet: str) -> str: + """ + Apply only conservative wording cleanup. + + No new facts, metrics, technologies, responsibilities, + or outcomes are introduced here. + """ + cleaned = _clean(bullet) + + replacements = ( + ( + r"(?i)^was responsible for processing\s+", + "Processed ", + ), + ( + r"(?i)^responsible for processing\s+", + "Processed ", + ), + ( + r"(?i)^was responsible for creating\s+", + "Created ", + ), + ( + r"(?i)^responsible for creating\s+", + "Created ", + ), + ( + r"(?i)^was responsible for developing\s+", + "Developed ", + ), + ( + r"(?i)^responsible for developing\s+", + "Developed ", + ), + ) + + for pattern, replacement in replacements: + if re.search(pattern, cleaned): + cleaned = re.sub( + pattern, + replacement, + cleaned, + count=1, + ) + break + + return _ensure_period(cleaned) + + +def _metric_to_result_clause(metric_text: str) -> str: + """ + Convert a user-supplied result into a clause that can be appended + without changing its factual content. + """ + metric = _clean(metric_text).rstrip(".") + + conversions = ( + (r"(?i)^reduced\s+", "reducing "), + (r"(?i)^saved\s+", "saving "), + (r"(?i)^increased\s+", "increasing "), + (r"(?i)^improved\s+", "improving "), + (r"(?i)^decreased\s+", "decreasing "), + (r"(?i)^cut\s+", "cutting "), + ) + + for pattern, replacement in conversions: + if re.search(pattern, metric): + return re.sub( + pattern, + replacement, + metric, + count=1, + ) + + if metric: + return metric[0].lower() + metric[1:] + + return metric + + +def propose_rewrite( + bullet: str, + supplied_metric: str | None = None, + requested_claim: str | None = None, +) -> RewriteProposal: + """ + Produce an evidence-first rewrite proposal. + + Rules: + 1. Never invent quantitative impact. + 2. Never add unsupported leadership claims. + 3. User-supplied metrics must contain explicit quantitative evidence. + 4. All evidence used is returned for traceability. + """ + source = _clean(bullet) + + if not source: + return RewriteProposal( + original_bullet="", + proposed_bullet=None, + status="unsupported", + reason="A source bullet is required before a rewrite can be proposed.", + ) + + if requested_claim: + supported, reason = request_is_supported( + source, + requested_claim, + ) + + if not supported: + return RewriteProposal( + original_bullet=source, + proposed_bullet=None, + status="unsupported", + reason=reason, + ) + + source_analysis = analyze_bullet(source) + evidence_used = [ + item.text + for item in source_analysis.evidence + ] + + metric = _clean(supplied_metric or "") + + if not source_analysis.has_quantified_evidence and not metric: + return RewriteProposal( + original_bullet=source, + proposed_bullet=None, + status="needs_metric", + reason=( + "The source bullet does not contain a measurable result. " + "No metric will be invented." + ), + evidence_used=evidence_used, + questions=[metric_question()], + ) + + if metric: + metric_evidence = extract_evidence(metric) + + if not metric_evidence: + return RewriteProposal( + original_bullet=source, + proposed_bullet=None, + status="needs_metric", + reason=( + "The supplied impact statement does not contain an " + "explicit measurable result." + ), + evidence_used=evidence_used, + questions=[metric_question()], + ) + + evidence_used.extend( + item.text + for item in metric_evidence + ) + + rewritten = strengthen_structure(source).rstrip(".") + + if metric: + metric_clause = _metric_to_result_clause(metric) + + if metric_clause.lower() not in rewritten.lower(): + rewritten = f"{rewritten}, {metric_clause}" + + rewritten = _ensure_period(rewritten) + + return RewriteProposal( + original_bullet=source, + proposed_bullet=rewritten, + status="ready", + reason=( + "Rewrite uses only facts and measurable evidence supplied " + "by the resume or the user." + ), + evidence_used=list(dict.fromkeys(evidence_used)), + ) \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/requirements.txt b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/requirements.txt new file mode 100644 index 00000000..b6cd845a --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/requirements.txt @@ -0,0 +1,5 @@ +streamlit +python-docx +pytest +requests +python-dotenv \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/sample_documents/generate_sample_resume.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/sample_documents/generate_sample_resume.py new file mode 100644 index 00000000..751f7f30 --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/sample_documents/generate_sample_resume.py @@ -0,0 +1,52 @@ +from pathlib import Path + +from docx import Document + + +OUTPUT_DIR = Path(__file__).parent +OUTPUT_PATH = OUTPUT_DIR / "sample_resume.docx" + + +def build_resume() -> None: + document = Document() + + document.add_heading("Alex Morgan", level=0) + document.add_paragraph( + "Data Analyst | Python | SQL | Power BI" + ) + + document.add_heading("Experience", level=1) + + document.add_heading( + "Operations Data Analyst — Example Industries", + level=2, + ) + document.add_paragraph( + "January 2024 — Present" + ) + + bullets = [ + "Automated invoice processing using Python.", + "Processed 500 invoices per month using Python.", + "Built Power BI dashboards for operational reporting.", + "Was responsible for processing 1,200 records per week.", + "Reduced report preparation time from 3 hours to 45 minutes.", + ] + + for bullet in bullets: + paragraph = document.add_paragraph( + style="List Bullet" + ) + paragraph.add_run(bullet) + + document.add_heading("Skills", level=1) + document.add_paragraph( + "Python, SQL, Power BI, Excel" + ) + + document.save(OUTPUT_PATH) + print(f"Created: {OUTPUT_PATH}") + + +if __name__ == "__main__": + build_resume() \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/sample_documents/sample_resume.docx b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/sample_documents/sample_resume.docx new file mode 100644 index 00000000..58970092 Binary files /dev/null and b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/sample_documents/sample_resume.docx differ diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/__init__.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/client.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/client.py new file mode 100644 index 00000000..0fb8428c --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/client.py @@ -0,0 +1,278 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +import requests +from dotenv import load_dotenv + + +load_dotenv() + + +class SuperDocsError(RuntimeError): + pass + + +class SuperDocsClient: + BASE_URL = "https://api.superdocs.app" + + def __init__( + self, + api_key: str | None = None, + timeout: int = 180, + ) -> None: + self.api_key = ( + api_key + or os.getenv("SUPERDOCS_API_KEY") + ) + + self.timeout = timeout + + if not self.api_key: + raise SuperDocsError( + "SUPERDOCS_API_KEY is missing. " + "Add it to your local .env file." + ) + + @property + def auth_headers(self) -> dict[str, str]: + return { + "Authorization": + f"Bearer {self.api_key}", + } + + @property + def json_headers(self) -> dict[str, str]: + return { + **self.auth_headers, + "Content-Type": "application/json", + } + + def upload_document( + self, + *, + path: str | Path, + session_id: str, + open_mode: str = "replace", + ) -> dict[str, Any]: + path = Path(path) + + if not path.exists(): + raise FileNotFoundError( + path + ) + + with path.open("rb") as file_handle: + response = requests.post( + ( + f"{self.BASE_URL}" + "/v1/documents/upload" + ), + headers=self.auth_headers, + files={ + "file": ( + path.name, + file_handle, + ) + }, + data={ + "session_id": + session_id, + + "open_mode": + open_mode, + }, + timeout=self.timeout, + ) + + if not response.ok: + raise SuperDocsError( + ( + f"Upload failed for " + f"{path.name}. " + f"HTTP " + f"{response.status_code}: " + f"{response.text}" + ) + ) + + return response.json() + + def chat( + self, + *, + session_id: str, + message: str, + document_id: str | None = None, + ) -> dict[str, Any]: + payload: dict[str, Any] = { + "session_id": session_id, + "message": message, + } + + if document_id is not None: + payload["document_id"] = document_id + + response = requests.post( + f"{self.BASE_URL}/v1/chat", + headers=self.json_headers, + json=payload, + timeout=self.timeout, + ) + + if not response.ok: + raise SuperDocsError( + "Chat request failed. " + f"HTTP {response.status_code}: " + f"{response.text}" + ) + + return response.json() + + def start_async_chat( + self, + *, + session_id: str, + message: str, + approval_mode: str = "ask_every_time", + ) -> dict[str, Any]: + response = requests.post( + f"{self.BASE_URL}/v1/chat/async", + headers=self.json_headers, + json={ + "session_id": session_id, + "message": message, + "approval_mode": approval_mode, + }, + timeout=self.timeout, + ) + + if not response.ok: + raise SuperDocsError( + "Failed to start Review request. " + f"HTTP {response.status_code}: " + f"{response.text}" + ) + + return response.json() + + def get_job( + self, + *, + job_id: str, + ) -> dict[str, Any]: + response = requests.get( + f"{self.BASE_URL}/v1/jobs/{job_id}", + headers=self.auth_headers, + timeout=self.timeout, + ) + + if not response.ok: + raise SuperDocsError( + "Failed to read job status. " + f"HTTP {response.status_code}: " + f"{response.text}" + ) + + return response.json() + + def approve_changes( + self, + *, + session_id: str, + job_id: str, + changes: list[dict[str, Any]], + approved: bool, + ) -> dict[str, Any]: + response = requests.post( + ( + f"{self.BASE_URL}/v1/chat/" + f"{session_id}/approve" + ), + headers=self.json_headers, + json={ + "job_id": job_id, + "approved": approved, + "changes": changes, + }, + timeout=self.timeout, + ) + + if not response.ok: + raise SuperDocsError( + "Failed to submit approval decision. " + f"HTTP {response.status_code}: " + f"{response.text}" + ) + + return response.json() + + def continue_job( + self, + *, + session_id: str, + job_id: str, + should_continue: bool, + ) -> dict[str, Any]: + response = requests.post( + ( + f"{self.BASE_URL}/v1/chat/" + f"{session_id}/continue" + ), + headers=self.json_headers, + json={ + "job_id": job_id, + "continue": should_continue, + }, + timeout=self.timeout, + ) + + if not response.ok: + raise SuperDocsError( + "Failed to continue job. " + f"HTTP {response.status_code}: " + f"{response.text}" + ) + + return response.json() + + def export_document( + self, + *, + session_id: str, + output_path: str | Path, + filename: str, + file_format: str = "docx", + ) -> Path: + output_path = Path(output_path) + + response = requests.post( + f"{self.BASE_URL}/v1/documents/export", + headers=self.json_headers, + json={ + "session_id": session_id, + "format": file_format, + "filename": filename, + }, + timeout=self.timeout, + ) + + if not response.ok: + raise SuperDocsError( + "Document export failed. " + f"HTTP {response.status_code}: " + f"{response.text}" + ) + + output_path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + output_path.write_bytes( + response.content + ) + + return output_path \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/parser.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/parser.py new file mode 100644 index 00000000..050e6719 --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/parser.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import json +from typing import Any + + +class SuperDocsParseError(ValueError): + pass + + +def parse_pending_changes( + value: Any, +) -> list[dict[str, Any]]: + """ + SuperDocs pending changes may arrive either: + - already as a list + - as a JSON-encoded string + + Never silently treat malformed content as an + empty successful diff. + """ + + if isinstance(value, str): + try: + value = json.loads(value) + except json.JSONDecodeError as exc: + raise SuperDocsParseError( + "pending_changes was a string " + "but was not valid JSON." + ) from exc + + if value is None: + return [] + + if not isinstance(value, list): + raise SuperDocsParseError( + "pending_changes must be a list " + "or a JSON-encoded list." + ) + + changes: list[dict[str, Any]] = [] + + for item in value: + if not isinstance(item, dict): + raise SuperDocsParseError( + "Every proposed change must be an object." + ) + + changes.append(item) + + return changes + + +def pending_changes_from_job( + job: dict[str, Any], +) -> list[dict[str, Any]]: + """ + Extract proposed changes from a SuperDocs job. + + Current Review jobs expose pending_changes inside + job["metadata"], but fallback locations are supported + for graceful compatibility. + """ + + metadata = job.get("metadata") + + if isinstance(metadata, dict): + value = metadata.get( + "pending_changes" + ) + + if value is not None: + return parse_pending_changes( + value + ) + + for key in ( + "pending_changes", + "result", + "data", + "output", + ): + value = job.get(key) + + if key in { + "result", + "data", + "output", + }: + if isinstance(value, dict): + value = value.get( + "pending_changes" + ) + else: + continue + + if value is not None: + return parse_pending_changes( + value + ) + + return [] \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/polling.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/polling.py new file mode 100644 index 00000000..4cd6c211 --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/superdocs/polling.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import time +from typing import Any + +from superdocs.client import ( + SuperDocsClient, + SuperDocsError, +) + + +def wait_until_action_needed( + *, + client: SuperDocsClient, + job_id: str, + timeout_seconds: int = 180, + poll_interval: float = 2.0, +) -> dict[str, Any]: + started = time.monotonic() + last_status = None + + while True: + job = client.get_job( + job_id=job_id + ) + + status = job.get("status") + + if status != last_status: + print( + f" Job status: {status}" + ) + last_status = status + + if status in { + "awaiting_approval", + "completed", + }: + return job + + if status == "failed": + raise SuperDocsError( + job.get( + "error", + "SuperDocs job failed.", + ) + ) + + if status == "cancelled": + raise SuperDocsError( + "SuperDocs job was cancelled." + ) + + elapsed = ( + time.monotonic() + - started + ) + + if elapsed >= timeout_seconds: + raise TimeoutError( + f"Job {job_id} did not reach " + f"an actionable state within " + f"{timeout_seconds} seconds." + ) + + if elapsed >= 30: + print( + f" Still processing... " + f"{int(elapsed)}s elapsed" + ) + + time.sleep( + poll_interval + ) \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/tests/test_evidence.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/tests/test_evidence.py new file mode 100644 index 00000000..7ea612cf --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/tests/test_evidence.py @@ -0,0 +1,66 @@ +from core.evidence import analyze_bullet, extract_evidence, metric_question + + +def test_detects_volume_metric(): + bullet = "Processed 500 invoices per month using Python." + + analysis = analyze_bullet(bullet) + + assert analysis.has_quantified_evidence is True + assert any(item.text == "500 invoices" for item in analysis.evidence) + + +def test_missing_metric_is_reported(): + bullet = "Automated invoice processing using Python." + + analysis = analyze_bullet(bullet) + + assert analysis.has_quantified_evidence is False + assert "quantified impact" in analysis.missing_evidence + + +def test_detects_percentage(): + evidence = extract_evidence( + "Reduced processing time by 40% using Python automation." + ) + + assert any( + item.kind == "percentage" and item.text == "40%" + for item in evidence + ) + + +def test_detects_currency(): + evidence = extract_evidence( + "Reduced annual operating costs by $25,000." + ) + + assert any( + item.kind == "currency" and item.text == "$25,000" + for item in evidence + ) + + +def test_detects_duration_metrics(): + evidence = extract_evidence( + "Reduced processing time from 3 hours to 45 minutes." + ) + + values = [item.text for item in evidence] + + assert "3 hours" in values + assert "45 minutes" in values + + +def test_empty_bullet_has_no_evidence(): + analysis = analyze_bullet("") + + assert analysis.has_quantified_evidence is False + assert analysis.evidence == [] + + +def test_metric_question_does_not_suggest_fake_number(): + question = metric_question() + + assert "Do you know" in question + assert "measurable result" in question \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/tests/test_rewriter.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/tests/test_rewriter.py new file mode 100644 index 00000000..e087298d --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/tests/test_rewriter.py @@ -0,0 +1,99 @@ +from core.rewriter import ( + propose_rewrite, + request_is_supported, + strengthen_structure, +) + + +def test_missing_metric_requires_user_input(): + proposal = propose_rewrite( + "Automated invoice processing using Python." + ) + + assert proposal.status == "needs_metric" + assert proposal.proposed_bullet is None + assert proposal.questions + assert "No metric will be invented" in proposal.reason + + +def test_existing_metric_allows_rewrite(): + proposal = propose_rewrite( + "Processed 500 invoices per month using Python." + ) + + assert proposal.status == "ready" + assert proposal.proposed_bullet is not None + assert "500 invoices" in proposal.proposed_bullet + assert "500 invoices" in proposal.evidence_used + + +def test_user_supplied_metric_is_used_exactly(): + proposal = propose_rewrite( + "Automated invoice processing using Python.", + supplied_metric=( + "Reduced processing time from 3 hours to 45 minutes." + ), + ) + + assert proposal.status == "ready" + assert proposal.proposed_bullet is not None + assert "3 hours" in proposal.proposed_bullet + assert "45 minutes" in proposal.proposed_bullet + + assert "3 hours" in proposal.evidence_used + assert "45 minutes" in proposal.evidence_used + + +def test_non_quantified_user_answer_is_not_accepted(): + proposal = propose_rewrite( + "Automated invoice processing using Python.", + supplied_metric="It became much faster.", + ) + + assert proposal.status == "needs_metric" + assert proposal.proposed_bullet is None + + +def test_unsupported_leadership_claim_is_rejected(): + proposal = propose_rewrite( + "Built Power BI dashboards for operational reporting.", + requested_claim="Make this say I led a five-person analytics team.", + ) + + assert proposal.status == "unsupported" + assert proposal.proposed_bullet is None + assert "leadership claim" in proposal.reason + + +def test_supported_leadership_language_passes_gate(): + supported, reason = request_is_supported( + "Led the analytics team and built Power BI dashboards.", + "Emphasize that I led the analytics team.", + ) + + assert supported is True + assert "supported" in reason + + +def test_conservative_structure_cleanup(): + result = strengthen_structure( + "Was responsible for processing 500 invoices per month using Python." + ) + + assert result == ( + "Processed 500 invoices per month using Python." + ) + + +def test_rewrite_does_not_add_unknown_metric(): + proposal = propose_rewrite( + "Processed 500 invoices per month using Python." + ) + + assert proposal.status == "ready" + + result = proposal.proposed_bullet or "" + + assert "40%" not in result + assert "50%" not in result + assert "$" not in result \ No newline at end of file diff --git a/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/tests/test_superdocs_parser.py b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/tests/test_superdocs_parser.py new file mode 100644 index 00000000..0889dbfd --- /dev/null +++ b/use-cases/Kalpesh1Sharma/impact-quantifying-bullet-rewriter/tests/test_superdocs_parser.py @@ -0,0 +1,101 @@ +import pytest + +from superdocs.parser import ( + SuperDocsParseError, + parse_pending_changes, +) + + +def test_pending_changes_accepts_list(): + changes = [ + { + "change_id": "change-1", + "operation": "replace", + } + ] + + assert ( + parse_pending_changes(changes) + == changes + ) + + +def test_pending_changes_parses_json_encoded_string(): + raw = """ + [ + { + "change_id": "change-1", + "operation": "replace" + } + ] + """ + + result = parse_pending_changes( + raw + ) + + assert len(result) == 1 + + assert ( + result[0]["change_id"] + == "change-1" + ) + + +def test_bad_json_does_not_become_empty_changes(): + with pytest.raises( + SuperDocsParseError + ): + parse_pending_changes( + "[broken" + ) + +from superdocs.parser import ( + pending_changes_from_job, +) + + +def test_pending_changes_found_inside_job_metadata(): + job = { + "status": "awaiting_approval", + "metadata": { + "pending_changes": [ + { + "operation": "replace", + "old_html": "

Before

", + "new_html": "

After

", + } + ] + }, + } + + changes = pending_changes_from_job( + job + ) + + assert len(changes) == 1 + assert ( + changes[0]["operation"] + == "replace" + ) + + +def test_metadata_pending_changes_can_be_json_string(): + job = { + "status": "awaiting_approval", + "metadata": { + "pending_changes": ( + '[{"operation":"replace"}]' + ) + }, + } + + changes = pending_changes_from_job( + job + ) + + assert len(changes) == 1 + assert ( + changes[0]["operation"] + == "replace" + ) \ No newline at end of file