Skip to content

Commit

Permalink
Merge pull request #2 from bhargavyagnik/dev
Browse files Browse the repository at this point in the history
Update : v0.0.3
  • Loading branch information
bhargavyagnik authored Nov 15, 2024
2 parents 25e9195 + 097f5c5 commit 1b5d8c7
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 53 deletions.
64 changes: 33 additions & 31 deletions src/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
import os
app = FastAPI()

master_prompt = """
You are a professional cover letter writer. Provide only the cover letter content without any additional commentary, explanations, or formatting instructions.
Do not included any extra text like [general purpose fill in the blank], or any other instructions in your response.
"""

# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["chrome-extension://*"],
allow_origins=["chrome-extension://*","http://localhost:8000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
Expand All @@ -27,7 +32,8 @@ class JobDetails(BaseModel):
class CoverLetterRequest(BaseModel):
job_details: JobDetails
resume_text: str
model: str = "llama-3.1-8b-instruct" # default value
model: str = "llama-3.1-8b-instruct"
system_prompt: str

@app.post("/upload-resume")
async def upload_resume(file: UploadFile):
Expand All @@ -50,33 +56,31 @@ async def upload_resume(file: UploadFile):
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

async def generate_cover_letter_stream(job_details: JobDetails,resume_text: str, model: str):
async def generate_cover_letter_stream(job_details: JobDetails, resume_text: str, model: str, system_prompt: str):
async with httpx.AsyncClient() as client:
try:

if model == "llama-3.1-8b-instruct":

response = await client.post(
"https://api.perplexity.ai/chat/completions",
headers={
"Authorization": f"Bearer {os.getenv('PERPLEXITY_API_KEY')}",
"Content-Type": "application/json"
},
json={
"model": model,
"messages": [
{
"role": "system",
"content": "You are a professional cover letter writer. Provide only the cover letter content without any additional commentary, explanations, or formatting instructions. Start with 'Dear Hiring Manager,'. and end with 'Sincerely,'. Do not included [general purpose fill in the blank] in your response."
},
{
"role": "user",
"content": f"Write a professional cover letter for a {job_details.title} position at {job_details.company}. Use my resume details to highlight relevant experience. Format it as a standard business letter. Job description: {job_details.description}. Resume: {resume_text}"
}
],
"stream": True
},
timeout=None)
"Authorization": f"Bearer {os.getenv('PERPLEXITY_API_KEY')}",
"Content-Type": "application/json"
},
json={
"model": model,
"messages": [
{
"role": "system",
"content": master_prompt+system_prompt.format(position=job_details.title, company=job_details.company, description=job_details.description)
},
{
"role": "user",
"content": f"Write a professional cover letter for a {job_details.title} position at {job_details.company}. Use my resume details to highlight relevant experience. Format it as a standard business letter. Job description: {job_details.description}. Resume: {resume_text}"
}
],
"stream": True
},
timeout=None)
async for line in response.aiter_lines():
if line.startswith("data: "):
try:
Expand All @@ -85,20 +89,19 @@ async def generate_cover_letter_stream(job_details: JobDetails,resume_text: str,
yield f"data: {json.dumps({'content': content})}\n\n"
except json.JSONDecodeError:
continue
elif model =='mixtral-8x7b-32768':
print(f"Bearer {os.getenv('GROQ_API_KEY')}")
else:
response = await client.post(
"https://api.groq.com/openai/v1/chat/completions",
headers={
"Authorization": f"Bearer {os.getenv('GROQ_API_KEY')}",
"Content-Type": "application/json"
},
json={
"model": model,
"model": "mixtral-8x7b-32768",
"messages": [
{
"role": "system",
"content": "You are a professional cover letter writer. Provide only the cover letter content without any additional commentary, explanations, or formatting instructions. Start with 'Dear Hiring Manager,'. and end with 'Sincerely,'. Do not included [general purpose fill in the blank] in your response."
"content": master_prompt+system_prompt.format(position=job_details.title, company=job_details.company, description=job_details.description)
},
{
"role": "user",
Expand All @@ -108,16 +111,14 @@ async def generate_cover_letter_stream(job_details: JobDetails,resume_text: str,
"stream": True,
})
async for line in response.aiter_lines():
print(line)
if line.startswith("data: "):
try:
if line.strip() == "data: [DONE]":
break

data = json.loads(line[6:]) # Remove "data: " prefix
if content := data.get("choices", [{}])[0].get("delta", {}).get("content"):
if content.strip(): # Only yield non-empty content
yield f"data: {json.dumps({'content': content})}\n\n"
yield f"data: {json.dumps({'content': content})}\n\n"
except json.JSONDecodeError:
continue

Expand All @@ -132,7 +133,8 @@ async def generate_cover_letter(request: CoverLetterRequest):
generate_cover_letter_stream(
request.job_details,
request.resume_text,
request.model
request.model,
request.system_prompt
),
media_type="text/event-stream"
)
Expand Down
27 changes: 19 additions & 8 deletions src/extension/background.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
const DEFAULT_SYSTEM_PROMPT = `Start with 'Dear Hiring Manager,' and end with 'Sincerely,' followed by just name.
Paragraph 1: Introduction. Who you are, what you do, and what position you're applying for.
Paragraph 2: Summarize your experience.
Paragraph 3: How your experience translates to the job.
Paragraph 4: Closing and contact information.`;


chrome.runtime.onInstalled.addListener(() => {
console.log('Coverquai Extension installed');
});
Expand Down Expand Up @@ -29,10 +36,10 @@ chrome.runtime.onInstalled.addListener(() => {

async function handleLLMRequest(data, port) {
try {
// Get settings from storage
const settings = await chrome.storage.sync.get({
defaultModel: 'llama-3.1-8b-instruct',
resumeText: data.resumeData // Fall back to provided resume if no default
resumeText: data.resumeData,
systemPrompt: DEFAULT_SYSTEM_PROMPT
});

const response = await fetch('https://cvwriter-bhargavyagniks-projects.vercel.app/generate-cover-letter', {
Expand All @@ -44,19 +51,21 @@ chrome.runtime.onInstalled.addListener(() => {
body: JSON.stringify({
job_details: data.jobDetails,
resume_text: settings.resumeText,
model: settings.defaultModel // Add model to your API request
model: settings.defaultModel,
system_prompt: settings.systemPrompt
})
});

if (!response.ok) {
throw new Error(`API request failed: ${response.statusText}`);
}

// Create a new ReadableStream from the response
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();

let buffer = '';

while (true) {
const { done, value } = await reader.read();

Expand All @@ -65,9 +74,12 @@ chrome.runtime.onInstalled.addListener(() => {
break;
}

// Split the chunk into individual SSE messages
const messages = value.split('\n\n');

buffer += value;
console.log('Buffer content:', buffer);
const messages = buffer.split('\n\n');
console.log('Messages:', messages);
buffer = messages.pop() || '';

for (const message of messages) {
if (!message.trim() || !message.startsWith('data: ')) continue;

Expand Down Expand Up @@ -96,7 +108,6 @@ chrome.runtime.onInstalled.addListener(() => {
error: 'Failed to generate cover letter: ' + error.message
});
} finally {
// Ensure we always send a done message
port.postMessage({ type: 'done' });
}
}
2 changes: 1 addition & 1 deletion src/extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Coverquai",
"version": "0.0.2",
"version": "0.0.3",
"description": "Automatically generate cover letters based on job descriptions",
"icons": {
"16": "icons/icon16.png",
Expand Down
10 changes: 9 additions & 1 deletion src/extension/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,18 @@ <h1>Settings</h1>
<label for="modelSelect">Default AI Model:</label>
<select id="modelSelect">
<option value="llama-3.1-8b-instruct">Llama 3.1 8B</option>
<!-- <option value="mixtral-8x7b-32768">Mixtral 8x7B</option> -->
<option value="mixtral-8x7b-32768">Mixtral 8x7B</option>
<option value="llama-3.1-8b-instant">Llama 3.1 8B (Faster)</option>
</select>
</div>

<div class="setting-group">
<label for="systemPrompt">AI System Prompt:</label>
<textarea id="systemPrompt" rows="8" style="width: 96%; padding: 12px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 14px; resize: vertical;"></textarea>
<button id="resetPrompt" style="margin-top: 8px; background-color: #64748b;">Reset to Default</button>
<p class="help-text">Customize how the AI generates cover letters. Use {position}, {company}, and {description} as placeholders.</p>
</div>

<div class="button-group">
<button id="save">Save Settings</button>
<span id="status"></span>
Expand Down
22 changes: 20 additions & 2 deletions src/extension/options.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
const DEFAULT_SYSTEM_PROMPT = `Start with 'Dear Hiring Manager,' and end with 'Sincerely,' followed by just name.
Paragraph 1: Introduction. Who you are, what you do, and what position you're applying for.
Paragraph 2: Summarize your experience.
Paragraph 3: How your experience translates to the job.
Paragraph 4: Closing and contact information.`;


// Saves options to chrome.storage
function saveOptions() {
const modelSelect = document.getElementById('modelSelect').value;
const coverLetterName = document.getElementById('coverLetterName').value;
const systemPrompt = document.getElementById('systemPrompt').value;
const resumeInput = document.getElementById('defaultResume');
const status = document.getElementById('status');

Expand All @@ -27,6 +35,7 @@ function saveOptions() {
chrome.storage.sync.set({
defaultModel: modelSelect,
coverLetterName: coverLetterName,
systemPrompt: systemPrompt,
resumePath: file.name,
resumeText: data.resume_text
}, resolve);
Expand All @@ -41,7 +50,8 @@ function saveOptions() {
// Save without updating resume
chrome.storage.sync.set({
defaultModel: modelSelect,
coverLetterName: coverLetterName
coverLetterName: coverLetterName,
systemPrompt: systemPrompt
}, resolve);
}
});
Expand All @@ -61,10 +71,12 @@ function restoreOptions() {
chrome.storage.sync.get({
defaultModel: 'llama-3.1-8b-instruct',
coverLetterName: 'cover-letter-{company}',
resumePath: ''
resumePath: '',
systemPrompt: DEFAULT_SYSTEM_PROMPT
}, (items) => {
document.getElementById('modelSelect').value = items.defaultModel;
document.getElementById('coverLetterName').value = items.coverLetterName;
document.getElementById('systemPrompt').value = items.systemPrompt;
document.getElementById('currentResumePath').textContent =
items.resumePath ? `Current resume: ${items.resumePath}` : 'No resume selected';
});
Expand All @@ -78,4 +90,10 @@ document.getElementById('coverLetterName').addEventListener('input', function(e)
const preview = e.target.value.replace('{company}', 'Example Corp');
const helpText = e.target.nextElementSibling;
helpText.textContent = `Preview: ${preview}.pdf`;
});

// Add reset prompt handler
document.getElementById('resetPrompt').addEventListener('click', function() {
document.getElementById('systemPrompt').value = DEFAULT_SYSTEM_PROMPT;
saveOptions();
});
24 changes: 14 additions & 10 deletions src/extension/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,16 +266,20 @@ document.addEventListener('DOMContentLoaded', async function() {

function formatCoverLetter(text) {
return text
// Replace multiple newlines with proper paragraph breaks
.replace(/\n{2,}/g, '</p><p>')
// Replace single newlines with line breaks
.replace(/\n/g, '<br>')
// Wrap in paragraphs if not already wrapped
.replace(/^(.+)$/, '<p>$1</p>')
// Clean up any empty paragraphs
.replace(/<p>\s*<\/p>/g, '')
// Ensure consistent spacing around paragraphs
.replace(/<\/p><p>/g, '</p>\n\n<p>');
// Ensure consistent paragraph breaks
.replace(/([.!?])\s+([A-Z])/g, '$1</p><p>$2')
// Handle the signature line
.replace(/(Sincerely,.*?[A-Za-z]+)/, '</p><p>$1</p>')
// Replace remaining newlines with line breaks
.replace(/\n/g, '<br>')
// Wrap in paragraphs if not already wrapped
.replace(/^(.+)$/, '<p>$1</p>')
// Clean up any empty paragraphs
.replace(/<p>\s*<\/p>/g, '')
// Ensure consistent spacing around paragraphs
.replace(/<\/p><p>/g, '</p>\n\n<p>')
// Clean up any duplicate breaks
.replace(/<br\s*\/?>\s*<br\s*\/?>/g, '<br>');
}


Expand Down

0 comments on commit 1b5d8c7

Please sign in to comment.