cicero_job_apps / app.py
arthrod's picture
kljlk
76b80b6
raw
history blame
24.9 kB
import marimo
__generated_with = "0.17.0"
app = marimo.App()
@app.cell
def _():
import marimo as mo
mo.md('# <center>Cicero Jobs</center>')
return (mo,)
@app.cell
def _():
import glob
import os
from pydantic_ai import Agent, BinaryContent, DocumentUrl
from pydantic import BaseModel, Field
return Agent, BaseModel, BinaryContent, DocumentUrl, 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"""
<JobApplicationGenerationRequest>
<Persona>
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.
</Persona>
<InputData>
<Resume>
{resume}
</Resume>
<JobOpening>
{cover_letter}
</JobOpening>
<FurtherInstructions>
{fi}
</FurtherInstructions>
</InputData>
<OutputSpecification>
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.
</OutputSpecification>
<CoreProcessingInstructions>
1. **Analyze and Synthesize:** Meticulously analyze the provided `<Resume>` and the `<JobOpening>`. 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 `<WritingGuidelines>` 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 `<Resume>`. 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.
</CoreProcessingInstructions>
<GenerationTasks>
<Task id="resume">
<Description>
Generate a complete, richly formatted Markdown resume, fully tailored for the specific job opening.
</Description>
<Instructions>
- 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)
- MANDATORY: headers such as the candidate name, section titles (e.g., Summary, Experience, Skills), and contact information must be wrapped in <p style=\"text-align:center;\"> ... </p> tags.
- MANDATORY: Must end wishing the candidate good luck and explaining finding a job is a numbers game. Motivate them!
</Instructions>
</Task>
<Task id="cover_letter">
<Description>
Generate a compelling, personalized, and richly formatted Markdown cover letter.
</Description>
<Instructions>
- 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.
</Instructions>
</Task>
<Task id="letter_to_recruiter">
<Description>
Generate a concise, professional, and richly formatted Markdown message for LinkedIn or email outreach.
</Description>
<Instructions>
- 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.
</Instructions>
</Task>
<Task id="tips">
<Description>
Generate 5-7 actionable tips specific to this application, formatted as a bulleted list in Markdown.
</Description>
<Instructions>
- 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.
</Instructions>
</Task>
</GenerationTasks>
<WritingGuidelines>
<Section name="Summary">
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.
</Section>
<Section name="Work Experience">
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.
</Section>
<Section name="Bullet Point Structuring">
- 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.
</Section>
<Section name="Technical Skills">
- 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%."
</Section>
<Section name="Soft Skills">
- 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."
</Section>
<Section name="Professional Brand and Summary Details">
- 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."
</Section>
<Section name="Experience Section Details">
- 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).
</Section>
</WritingGuidelines>
</JobApplicationGenerationRequest>
"""
@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')
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')
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 (or press Ctrl/Cmd Enter)', 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):
continue_render
return
@app.cell
def _(job_posting_area, job_url, mo):
if job_posting_area.value != '':
buceta = 'Vamo'
job_posting = job_posting_area.value
elif job_posting_area.value == '' and job_url.value == '':
buceta = None
job_posting = None
elif job_posting_area.value == '' and job_url.value != '':
buceta = 'Vamo'
import httpx
url = f'https://r.jina.ai/{job_url.value}'
import 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
mo.stop(buceta is None, mo.callout(mo.md('⚠️ **Please provide your materials before continuing!**'), kind='danger'))
return (job_posting,)
@app.cell
def _(resume_button):
# Helper function to determine media type from filename
import io
try:
if resume_button.contents() is not '':
print(f'we got button contents: {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)
print('Converted to markdown')
except Exception as e:
print(f'an exception occurrred {e}')
if isinstance(resume_contents, (bytes, bytearray)):
resume_complete = io.BytesIO(resume_contents)
else:
resume_complete = resume_contents
else:
resume_contents = ''
except Exception:
resume_contents = ''
return 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 _(BinaryContent, DocumentUrl, job_posting_area, job_url):
job_posting_complete = ''
job_posting_mime = 'text/plain'
is_url = False
job_area_value = job_posting_area.value if hasattr(job_posting_area, 'value') else None
job_url_value = job_url.value if hasattr(job_url, 'value') else None
if job_area_value:
job_posting_complete = job_area_value
job_posting_mime = 'text/plain'
is_url = False
elif job_url_value:
job_posting_complete = job_url_value.strip()
# Basic URL detection
if job_posting_complete.lower().startswith(('http://', 'https://')):
is_url = True
job_posting_mime = None
else:
# If user provided something in the URL field that's not an HTTP URL, treat it as plain text
is_url = False
job_posting_mime = 'text/plain'
else:
job_posting_complete = ''
job_posting_mime = 'text/plain'
is_url = False
# Build inputs for the agent
if is_url:
job_posting_input = DocumentUrl(job_posting_complete)
else:
job_posting_input = BinaryContent(data=job_posting_complete, media_type=job_posting_mime or 'text/plain')
return
@app.cell
def _(resume_complete):
print(resume_complete)
return
@app.cell
def _(Agent, ApplicationMaterials, final_prompt):
import asyncio
import uvloop
import nest_asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
nest_asyncio.apply()
career_agent = Agent('gemini-2.5-flash', output_type=ApplicationMaterials)
if final_prompt is not None:
result = asyncio.run(career_agent.run(final_prompt))
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('_') or 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 _():
return
@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):
preview_section
return
@app.cell
def _(io):
from html4docx import HtmlToDocx
import mistune
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)
print('Successfully converted markdown to DOCX using html4docx with mistune')
return docx_bytes.getvalue()
except Exception as e:
print("Couldn't do it")
return markdown_text
except Exception as e:
print("Couldn't do it")
return markdown_text
return (markdown_to_docx_bytes,)
@app.cell
def _(markdown_to_docx_bytes, mo, processed_data):
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
return
@app.cell
def _(mo):
mo.md('# <center>Time to do another application! You got this!')
return
if __name__ == "__main__":
app.run()