import contextlib
import marimo
__generated_with = '0.17.0'
app = marimo.App()
with app.setup:
import subprocess
import sys
with contextlib.suppress(Exception):
subprocess.run([sys.executable, '-m', 'pip', 'install', 'uv'], check=True)
with contextlib.suppress(Exception):
subprocess.run([sys.executable, '-m', 'uv', 'pip', 'install', '-r', 'requirements.txt'], check=True)
@app.cell
def _():
import marimo as mo
mo.md("""
#
Cicero Jobs
## How to Use This App:
1. **Upload Your Resume**: Drop a PDF/DOC/DOCX/TXT file or paste your resume text
2. **Add Job Posting**: Paste the job description or provide the job posting URL
3. **Click Submit**: Wait while AI generates your materials (takes about 30-60 seconds)
4. **Review & Download**: Preview all documents and download as Word files
## What You'll Get:
- **Tailored Resume**: Your resume optimized with keywords from the job posting
- **Cover Letter**: Personalized 3-4 paragraph letter highlighting relevant experience
- **Recruiter Message**: Professional 5-7 sentence outreach message for LinkedIn/email
- **Application Tips**: 5-7 actionable tips specific to this job application
""")
return (mo,)
@app.cell
def _():
from pydantic import BaseModel, Field
from pydantic_ai import Agent
return Agent, BaseModel, Field
@app.function
def create_job_application_prompt(resume: str, cover_letter: str, further_instructions: str | None = None) -> str:
fi = further_instructions or ''
return f"""
You are an expert human resources professional and a resume and cover letter drafting specialist, with a deep focus on the in-house legal market. Your task is to generate a complete set of application materials based on the provided resume and job description.
{resume}
{cover_letter}
{fi}
Your final output must be a structured object with four distinct fields: `resume`, `cover_letter`, `letter_to_recruiter`, and `tips`. Follow the detailed instructions for each field below.
1. **Analyze and Synthesize:** Meticulously analyze the provided `` and the ``. Your primary goal is to tailor the candidate's materials to perfectly align with the requirements and keywords of the job description.
2. **Adhere to Writing Guidelines:** In all generated content, strictly apply the principles outlined in the `` section. This includes using appropriate tense, structuring bullet points effectively (PAR, STAR), incorporating action verbs, and maintaining a professional brand.
3. **Fact-Based Generation:** Do not invent skills or experiences. All generated content must be directly supported by the information in the provided ``. If a skill is treated superficially, you may suggest enhancements or ask clarifying questions in the `tips` section.
4. **Keyword Integration:** Identify core competencies, role-specific skills, and industry-relevant keywords from the job posting. Systematically integrate these exact keywords (avoiding synonyms unless instructed) into all generated documents to optimize for Applicant Tracking Systems (ATS) and recruiter review.
Generate a complete, richly formatted Markdown resume, fully tailored for the specific job opening.
- Return the **entire, updated resume**, not just the changed parts. The final output should be a single, complete Markdown document.
- **Summary Section:** Draft a concise (30-90 words) and impactful summary. This is your elevator pitch.
- If not extremely excessive, match the professional title based on the job opening:
- DO: "Commercial Counsel" -> "Commercial and Corporate Attorney"
- DO: "Corporate Counsel" -> "Corporate Attorney"
- DO NOT: Attorney --> General Counsel
- DO NOT: intern --> manager
- Incorporate the candidate's years of experience, but adjust as follows:
- If the required experience is incompatible with the candidate's seniority, use phrases like "seasoned" or "experienced" instead of a number.
- For roles requiring 3-4 years, do not overemphasize seniority.
- For roles requiring 5+ years, explicitly state the number of years of experience.
- **Skills Section:**
- Identify the most important keywords from the job posting. Prioritize keywords that appear multiple times, are standard for the role, or showcase unique relevant skills.
- Include industry-standard skills that may not be in the job description but are valuable for the role.
- Format the skills as a comma-separated list: `Skilla, skillb, skillc`.
- **Experience Section:**
- Adapt the language in the resume to use the exact keywords from the job posting, replacing any synonyms.
- Ensure bullet points are unique, highlight quantifiable achievements, and provide context (e.g., team size, project value).
- Structure bullet points using the C.A.R. technique, front-loading the result where possible.
- **Formatting:** Ensure the length and structure of the updated resume are similar to the original provided document.
- MANDATORY: Your answer must be perfect Markdown syntax (will be rendered by mistune). Do NOT use h1 (e.g.\\#", but use the styles appropriately (like all caps for name, sections, bold and italics where appropriate)!
Generate a compelling, personalized, and richly formatted Markdown cover letter.
- The cover letter must be under 400 words and structured in 3-4 paragraphs.
- Address it to the hiring manager. If a name is available in the job posting, use it.
- The content should be approximately 60% based on the job opening and 40% on the resume.
- Explicitly mention experiences from the resume that are directly relevant to the role. Highlight leadership experience (e.g., managing paralegals).
- When mentioning past employers, do not append suffixes like "LLP" or "Inc."
- Perform a quick web search for information about the company to add a sentence or two showing genuine interest and research.
- Ensure all statements are directly supported by the resume.
Generate a concise, professional, and richly formatted Markdown message for LinkedIn or email outreach.
- The message should be 5-7 sentences long.
- Draft a clear and professional subject line (e.g., "Regarding the Corporate Counsel Position").
- Address the hiring manager by name if available.
- Briefly introduce the candidate, state the position being applied for, and highlight 1-2 key qualifications that make them a perfect fit.
- End with a clear call to action, such as expressing eagerness to discuss the role further.
- Return the message without any additional markup or bullet points.
Generate 5-7 actionable tips specific to this application, formatted as a bulleted list in Markdown.
- Analyze the resume against the job description to identify strengths and potential gaps.
- Provide concrete advice. For example:
- "During the interview, be prepared to elaborate on your experience with [Specific Skill from Job Posting] by using the project at [Previous Company] as a prime example."
- "The job emphasizes [Company Value or Mission]. Consider mentioning how your work in [Relevant Experience] aligns with this value."
- "Your resume mentions [Superficial Skill]. I recommend adding a bullet point with a quantifiable result to strengthen this, such as..."
- "Research the company's recent acquisition of [Company X] and be prepared to discuss its potential legal integration challenges."
- The tips should be formatted as a simple Markdown bulleted list.
Not always required: If you've included a cover letter, your summary might be redundant. Valuable for quickly grabbing attention and highlighting your most relevant qualifications, as well as filling the gap if you're not submitting a cover letter.
Focus on:
- Who you are and what you do: Clearly state your professional identity and area of expertise.
- Your achievements: Highlight measurable results, awards, problems solved, and times you exceeded expectations.
This is the section of your resume in which you detail your career history.
- Use bullets for skim value.
- Use simple present tense for current roles (e.g., manages) and simple past tense for past roles (e.g., managed).
- Provide context for each position: company type, industry, size, and your role's impact on the bottom line.
- Focus on your main functions and their relevance to the target job.
- Emphasize quantifiable impact: landing clients, saving costs, automating tasks, hitting targets.
- PAR (Problem-Action-Result): Ideal for showing accomplishments and impact. Example: "Reduced customer support response time by 20% by implementing a new ticketing system."
- STAR (Situation-Task-Action-Result): Provides a more detailed narrative for demonstrating specific competencies. Example: "Managed inventory levels in a fast-paced retail setting; achieved a 15% reduction in inventory costs."
- Result-First: Leads with the outcome to grab attention. Example: "Increased sales revenue by 18% through targeted marketing campaigns."
- Action Verb + Skill + Result: Concise and effective for technical roles. Example: "Developed a user-friendly website using HTML, CSS, and JavaScript, resulting in a 30% increase in website traffic."
- Prioritize your most relevant bullet points for each specific job application.
- Showcase specific knowledge in tools, software, and processes.
- Hard Skills: Programming languages (Java, Python), software (Adobe Creative Suite), cloud platforms (AWS, Azure).
- Soft Technical Skills: Troubleshooting, data analysis, project management, technical writing.
- Use the PAR method to demonstrate technical skills in context. Example: "Problem: Website traffic was declining. Action: Implemented optimization techniques using HTML, CSS, JavaScript. Result: Increased traffic by 25%."
- Avoid overused terms like "Problem-solving" or "Driven".
- Instead of listing soft skills, demonstrate them through your achievement-oriented bullet points. Example: Instead of "excellent communication skills," write "Presented quarterly performance updates to internal stakeholders."
- Your brand is the unique combination of your skills, experience, and personality.
- Your summary is your sales pitch, answering "Who are you and why should I hire you?"
- It should be concise (60-120 words), factual, data-driven, and placed at the top of your resume.
- Use a combination of present tense ("who you are") and past tense ("what you've achieved"). Example: "Project manager (PMP) with 11 years of experience... Successfully completed three plant projects in 2022 valued at $15MM."
- List your most relevant responsibilities and bullets first.
- Avoid generic statements; provide specific context and quantifiable metrics.
- Use the C.A.R (Challenge, Action, Result) technique. Where possible, front-load the bullet with the result.
- Use a short introductory paragraph for each role to provide a high-level overview.
- Utilize strong action verbs (e.g., Achieved, Coordinated, Implemented, Streamlined, Managed, Maximized).
"""
@app.cell(hide_code=True)
def _(BaseModel, Field):
class ApplicationMaterials(BaseModel):
"""Structured output for resume and cover letter generation."""
resume: str = Field(
...,
description='Markdown Richly Formatted resume highlighting relevant experience for the job. 2 pages MAX, aim for 1.',
)
cover_letter: str = Field(
..., description='Markdown Richly Formatted and Compelling, personalized cover letter (3-4 paragraphs)'
)
letter_to_recruiter: str = Field(
...,
description='Markdown Richly Formatted and Concise professional message for LinkedIn/email outreach (5-7 sentences)',
)
tips: str = Field(
...,
description='Markdown Richly Formatted and 5-7 actionable tips specific to this application, formatted as bullet points. MANDATORY: Must end wishing the candidate good luck and explaining finding a job is a numbers game. Motivate them!',
)
return (ApplicationMaterials,)
@app.cell(hide_code=True)
def _(mo):
resume_button = mo.ui.file(
kind='area',
filetypes=['.pdf', '.txt', '.doc', '.docx'],
label='Or drop your pdf/doc/docx/txt file résumé here!',
)
job_posting_area = mo.ui.text_area(placeholder='📋 Drop your job posting here!', full_width=True, rows=27)
resume_area = mo.ui.text_area(
placeholder='📄 Provide the full content of your résumé. Uploading file in pdf/docx/doc/txt is preferred.',
full_width=True,
rows=20,
)
job_url = mo.ui.text(
placeholder='https://example.com/job-posting',
label='Alternatively, provide the URL of the job posting',
full_width=True,
)
continue_button = mo.ui.button(
value=None,
kind='success',
tooltip='Click submit to continue.',
label='Submit All (it takes a little bit, please be patient!)',
keyboard_shortcut='Ctrl+Enter',
full_width=True,
)
continue_render = mo.vstack(
[
mo.md("# Let's start with your materials!"),
mo.vstack(
[
mo.md('## '),
mo.hstack(
[
mo.vstack([mo.md('### Resume:'), resume_button, resume_area], align='center'),
mo.vstack([mo.md('### Job Posting:'), job_posting_area, job_url], align='center'),
],
align='center',
),
continue_button,
],
align='start',
),
],
align='center',
)
return continue_render, job_posting_area, job_url, resume_button
@app.cell
def _(continue_render) -> None:
continue_render
@app.cell
def _(job_posting_area, job_url):
if job_posting_area.value != '':
job_posting = job_posting_area.value
buceta = True
elif job_posting_area.value == '' and job_url.value == '':
buceta = None
job_posting = None
elif job_posting_area.value == '' and job_url.value != '':
import httpx
url = f'https://r.jina.ai/{job_url.value}'
import os as _os
jina_api_key = _os.environ.get('JINA_API_KEY', '')
headers = {'Authorization': f'Bearer {jina_api_key}'}
response = httpx.get(url, headers=headers)
job_posting = response.text
buceta = True
else:
buceta = None
job_posting = None
return buceta, job_posting
@app.cell
def _(resume_button):
# Helper function to determine media type from filename
import io
try:
if resume_button.contents() != '':
resume_contents = resume_button.contents()
try:
from markitdown import MarkItDown
md = MarkItDown(enable_plugins=True)
forcing_bytes = io.BytesIO(resume_contents)
docx_converted = md.convert(forcing_bytes)
resume_complete = str(docx_converted.text_content)
buceta_ = True
except Exception:
if isinstance(resume_contents, (bytes, bytearray)):
resume_complete = io.BytesIO(resume_contents)
buceta_ = True
else:
resume_complete = resume_contents
buceta_ = True
else:
resume_contents = ''
buceta_ = None
except Exception:
resume_contents = ''
buceta_ = None
if resume_contents == '' or resume_contents is None:
resume_complete = None
return buceta_, io, resume_complete
@app.cell
def _(job_posting, resume_complete):
final_prompt = create_job_application_prompt(resume_complete, job_posting)
return (final_prompt,)
@app.cell
def _(buceta, buceta_):
bucetilda = buceta_ and buceta
return (bucetilda,)
@app.cell
def _(bucetilda, mo) -> None:
mo.stop(
bucetilda is None, mo.callout(mo.md('⚠️ **Please provide your materials before continuing!**'), kind='danger')
)
@app.cell
def _(Agent, ApplicationMaterials, bucetilda, final_prompt, mo):
mo.stop(bucetilda is None)
import asyncio
import nest_asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
nest_asyncio.apply()
career_agent = Agent('gemini-2.5-flash', output_type=ApplicationMaterials)
result = asyncio.run(career_agent.run(final_prompt)) if final_prompt is not None else None
return (result,)
@app.cell
def _(result):
import json
output = getattr(result, 'output', None)
parsed = None
if isinstance(output, str):
try:
parsed = json.loads(output)
except Exception:
parsed = None
elif isinstance(output, dict):
parsed = output
else:
try:
parsed = {}
for k in dir(output):
if k.startswith(('_', 'model_')):
continue
try:
v = getattr(output, k)
except Exception:
continue
if callable(v):
continue
parsed[k] = v
except Exception:
parsed = None
def _get_field(*keys, default=None):
if parsed and isinstance(parsed, dict):
for k in keys:
if k in parsed:
return parsed[k]
for k in keys:
if hasattr(output, k):
try:
return getattr(output, k)
except Exception:
continue
return default
processed_data = {
'resume': _get_field('resume', 'resume_text', default=None),
'cover_letter': _get_field('cover_letter', 'coverletter', 'cover_letter_text', default=None),
'recruiter_message': _get_field(
'letter_to_recruiter', 'recruiter_message', 'message_to_recruiter', default=None
),
'tips': _get_field('tips', 'advice', 'suggestions', default=None),
}
return (processed_data,)
@app.cell
def _(mo, processed_data):
preview_section = mo.vstack([
mo.md('## Preview Your Documents'),
mo.md('---'),
mo.md('### Here is your résumé, how it looks like?'),
mo.md('---'),
mo.md(processed_data.get('resume')),
mo.md('---'),
mo.md('---'),
mo.md('### 💼 Here is your Cover Letter'),
mo.md('---'),
mo.md(processed_data.get('cover_letter')),
mo.md('---'),
mo.md('---'),
mo.md('### 📧 Here is your message to reach out to the recruiter'),
mo.md('---'),
mo.md(processed_data.get('recruiter_message')),
mo.md('---'),
mo.md('---'),
mo.md('### 💡 Here are some additional tips'),
mo.md('---'),
mo.md(processed_data.get('tips')),
mo.md('---'),
mo.md('---'),
])
return (preview_section,)
@app.cell
def _(preview_section) -> None:
preview_section
@app.cell
def _(io):
import mistune
from html4docx import HtmlToDocx
from mistune.plugins.abbr import abbr
from mistune.plugins.def_list import def_list
from mistune.plugins.footnotes import footnotes
from mistune.plugins.formatting import insert, mark, strikethrough, subscript, superscript
from mistune.plugins.table import table
from mistune.plugins.task_lists import task_lists
# Define mistune plugins to use
MISTUNE_PLUGINS = [
strikethrough,
footnotes,
table,
task_lists,
insert,
def_list,
abbr,
mark,
subscript,
superscript,
]
def _build_markdown_parser(hard_wrap: bool = True):
"""Create a Mistune Markdown parser with desired plugins and hard-wrap behavior."""
renderer = mistune.HTMLRenderer()
inline = mistune.InlineParser(hard_wrap=hard_wrap)
return mistune.Markdown(renderer=renderer, inline=inline, plugins=MISTUNE_PLUGINS)
def markdown_to_docx_bytes(markdown_text: str) -> bytes | str:
"""Asynchronously converts markdown text to DOCX bytes with legal formatting."""
try:
markdown_parser = _build_markdown_parser(hard_wrap=True)
html_content = markdown_parser(markdown_text)
try:
parser = HtmlToDocx()
docx_document = parser.parse_html_string(html_content)
# Save to memory
docx_bytes = io.BytesIO()
docx_document.save(docx_bytes)
docx_bytes.seek(0)
return docx_bytes.getvalue()
except Exception:
return markdown_text
except Exception:
return markdown_text
return (markdown_to_docx_bytes,)
@app.cell
def _(markdown_to_docx_bytes, mo, processed_data) -> None:
final_resume = markdown_to_docx_bytes(processed_data.get('resume'))
final_cover_letter = markdown_to_docx_bytes(processed_data.get('cover_letter'))
final_recruiter_message = markdown_to_docx_bytes(processed_data.get('recruiter_message'))
resume_download = mo.download(data=final_resume, filename='resume.docx', label='Résumé')
cover_letter_download = mo.download(data=final_cover_letter, filename='cover_letter.docx', label='Cover Letter')
message_download = mo.download(
data=final_recruiter_message, filename='recruiter_message.docx', label='Recruiter Message'
)
downloads_section = mo.vstack([
mo.md('## 🎉 Your Documents Are Ready!'),
mo.md('Click the buttons below to download your documents:'),
mo.hstack([resume_download, cover_letter_download, message_download], justify='center', gap=2),
mo.md('---'),
mo.callout(
mo.md('✅ **Success!** Your documents have been generated and are ready to download.'), kind='success'
),
])
downloads_section
@app.cell
def _(mo) -> None:
mo.md("""# Time to do another application! Just add new materials. You got this!""")
if __name__ == '__main__':
app.run()