Spaces:
Running
Running
File size: 24,909 Bytes
22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 76b80b6 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 ca8de5d 22c7441 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 |
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()
|