Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,30 +1,424 @@
|
|
1 |
-
import
|
2 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request, jsonify
|
2 |
+
from werkzeug.utils import secure_filename
|
3 |
+
from openai import OpenAI
|
4 |
+
from io import BytesIO
|
5 |
+
import PyPDF2
|
6 |
+
from pdfminer.high_level import extract_text
|
7 |
+
from docx import Document
|
8 |
+
import os
|
9 |
+
import re
|
10 |
+
import uuid
|
11 |
+
from typing import Tuple
|
12 |
+
import pdfplumber
|
13 |
|
14 |
+
app = Flask(__name__)
|
15 |
+
app.config['UPLOAD_FOLDER'] = '/home/billy1129/resume_optimizer/static/uploads'
|
16 |
+
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
17 |
+
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB限制
|
18 |
|
19 |
+
# 初始化Azure OpenAI客户端
|
20 |
+
client = OpenAI(
|
21 |
+
base_url="https://api.deepseek.com",
|
22 |
+
api_key="sk-bc73223a36d240758af12bf4a197a3be"
|
23 |
+
)
|
24 |
|
25 |
+
def safe_filename(filename: str) -> str:
|
26 |
+
"""安全处理文件名,保留中文字符"""
|
27 |
+
filename = re.sub(r'[^\w\u4e00-\u9fff\-\.]', '', filename.strip())
|
28 |
+
name, ext = os.path.splitext(filename)
|
29 |
+
random_str = uuid.uuid4().hex[:6]
|
30 |
+
return f"{name}_{random_str}{ext}"
|
31 |
|
32 |
+
def extract_text_from_pdf(file_stream: BytesIO) -> str:
|
33 |
+
"""混合提取方案,增强去重处理(包括标点符号和括号)"""
|
34 |
+
def process_duplicates(text: str) -> str:
|
35 |
+
"""处理各种重复字符(中文、英文、标点、括号等)"""
|
36 |
+
# 处理中文重复(包括全角标点)
|
37 |
+
text = re.sub(r'([\u4e00-\u9fff])\1+', r'\1', text)
|
38 |
+
# 处理常见标点符号重复(包括全角和半角)
|
39 |
+
text = re.sub(r'([,。、;:"“”‘’\'\"\(\)\[\]\{\}\<\>])\1+', r'\1', text)
|
40 |
+
# 处理特殊重复模式(如"(("变成"(")
|
41 |
+
text = re.sub(r'(()\1+', r'\1', text)
|
42 |
+
text = re.sub(r'())\1+', r'\1', text)
|
43 |
+
return text
|
44 |
|
45 |
+
try:
|
46 |
+
# 优先尝试pdfplumber
|
47 |
+
try:
|
48 |
+
file_stream.seek(0)
|
49 |
+
with pdfplumber.open(file_stream) as pdf:
|
50 |
+
text = "\n".join([
|
51 |
+
page.extract_text(x_tolerance=2, y_tolerance=2)
|
52 |
+
for page in pdf.pages
|
53 |
+
if page.extract_text()
|
54 |
+
])
|
55 |
+
|
56 |
+
print("========= pdfplumber 原始提取内容 =========")
|
57 |
+
print(text)
|
58 |
+
|
59 |
+
text = process_duplicates(text)
|
60 |
+
|
61 |
+
print("========= 去重处理后内容 =========")
|
62 |
+
print(text)
|
63 |
+
return text.strip()
|
64 |
+
|
65 |
+
except Exception as e:
|
66 |
+
print(f"pdfplumber提取失败,尝试PyPDF2: {str(e)}")
|
67 |
+
# 备用方案:PyPDF2+去重
|
68 |
+
file_stream.seek(0)
|
69 |
+
reader = PyPDF2.PdfReader(file_stream)
|
70 |
+
text = '\n'.join({
|
71 |
+
line.strip()
|
72 |
+
for page in reader.pages
|
73 |
+
for line in (page.extract_text() or "").split('\n')
|
74 |
+
if line.strip()
|
75 |
+
})
|
76 |
+
|
77 |
+
print("========= PyPDF2 原始提取内容 =========")
|
78 |
+
print(text)
|
79 |
+
|
80 |
+
text = process_duplicates(text)
|
81 |
+
|
82 |
+
print("========= 去重处理后内容 =========")
|
83 |
+
print(text)
|
84 |
+
return text
|
85 |
+
|
86 |
+
except Exception as e:
|
87 |
+
raise ValueError(f"PDF解析失败: {str(e)}")
|
88 |
|
89 |
+
def extract_text_from_word(file_stream: BytesIO) -> str:
|
90 |
+
"""从Word文档提取文本"""
|
91 |
+
try:
|
92 |
+
file_stream.seek(0)
|
93 |
+
doc = Document(file_stream)
|
94 |
+
return "\n".join([para.text for para in doc.paragraphs if para.text])
|
95 |
+
except Exception as e:
|
96 |
+
raise ValueError(f"Word解析失败: {str(e)}")
|
97 |
|
98 |
+
def extract_text_from_file(file_stream: BytesIO, filename: str) -> str:
|
99 |
+
"""从上传文件提取文本内容"""
|
100 |
+
if filename.lower().endswith('.pdf'):
|
101 |
+
return extract_text_from_pdf(file_stream)
|
102 |
+
elif filename.lower().endswith(('.doc', '.docx')):
|
103 |
+
return extract_text_from_word(file_stream)
|
104 |
+
elif filename.lower().endswith('.txt'):
|
105 |
+
file_stream.seek(0)
|
106 |
+
return file_stream.read().decode('utf-8', errors='ignore')
|
107 |
+
else:
|
108 |
+
raise ValueError("不支持的文件格式")
|
109 |
+
|
110 |
+
def analyze_resume_with_ai(text: str, job_position: str = None) -> Tuple[list, int]:
|
111 |
+
"""使用OpenAI分析简历文本并评分"""
|
112 |
+
MAX_TOKENS = 120000
|
113 |
+
if len(text) > MAX_TOKENS * 3.5:
|
114 |
+
return ["简历内容过长,请简化内容"], 0
|
115 |
+
|
116 |
+
# 根据岗位生成针对性提示
|
117 |
+
job_specific_prompt = ""
|
118 |
+
if job_position:
|
119 |
+
job_specific_prompt = f"""
|
120 |
+
[岗位针对性分析]
|
121 |
+
目标岗位: {job_position}
|
122 |
+
请特别关注以下与目标岗位相关的评估维度:
|
123 |
+
1. 专业技能匹配度: 检查简历中是否包含该岗位的核心技能关键词
|
124 |
+
2. 项目经验相关性: 评估项目经验与目标岗位的匹配程度
|
125 |
+
3. 行业术语使用: 检查是否使用了该岗位领域的专业术语
|
126 |
+
4. 成就量化标准: 根据该岗位特点评估成就描述的量化程度
|
127 |
+
|
128 |
+
"""
|
129 |
+
prompt = f"""请严格按照以下四部分分析简历,严格遵循格式:
|
130 |
+
|
131 |
+
{job_specific_prompt if job_specific_prompt else ""}
|
132 |
+
[总扣分]
|
133 |
+
总扣分: XX分 # 必须单独一行明确写出总扣分值
|
134 |
+
|
135 |
+
[扣分项]
|
136 |
+
请列出简历的所有扣分项,每一项必须明确指出扣分项在简历中的位置,扣分数量,并给出具体改进建议,将扣分项和建议放在【缺点】中输出给用户。
|
137 |
+
请严格遵循以下评分标准中的扣分规则,最后在第一行,计算总扣分量,格式为"总扣分: XX分"。
|
138 |
+
|
139 |
+
[整体总结]
|
140 |
+
用一段话来整体概括这篇简历的优缺点,特别关注与目标岗位的匹配度。
|
141 |
+
|
142 |
+
[优点]
|
143 |
+
• 优点1 (特别标注与目标岗位相关的优势)
|
144 |
+
• 优点2
|
145 |
+
|
146 |
+
[缺点]
|
147 |
+
• 具体位置(简历第几行或哪个部分): 具体问题 (具体改进建议) (-X分)
|
148 |
+
• 具体位置(简历第几行或哪个部分): 具体问题 (具体改进建议) (-X分)
|
149 |
+
确保在[缺点]部分之后不输出任何其他内容
|
150 |
+
|
151 |
+
评分标准:
|
152 |
+
高质量简历评分标准(基于STAR法则和岗位匹配度)
|
153 |
+
一、基础信息完整性(满分15分)
|
154 |
+
必备信息要求:
|
155 |
+
姓名
|
156 |
+
联系方式(电话、邮箱)
|
157 |
+
住址信息(至少提供省份或城市)
|
158 |
+
评分细则:
|
159 |
+
每项必备信息均完整且正确:得满分15分。
|
160 |
+
缺失任一项:扣5分;如缺失两项及以上,累计扣分,但最低分为0分。
|
161 |
+
|
162 |
+
二、内容结构与逻辑性(满分25分)
|
163 |
+
结构要求:
|
164 |
+
简历需清晰划分区域,如个人信息、教育经历、工作经历、技能、项目经验等。
|
165 |
+
每一区块内容需符合逻辑,信息层次分明。
|
166 |
+
评分细则:
|
167 |
+
每个必备区域(至少5个区域)均明确标识并合理排序:每个区域得5分,区域缺失或模糊者扣5分。
|
168 |
+
在每个区域内,要求描述具备逻辑性和条理性,出现明显逻辑混乱(如叙述前后矛盾或顺序混乱)者,每处扣2分,累计扣分不超过该区域分值。
|
169 |
+
|
170 |
+
三、专业技能及关键词匹配(满分30分)
|
171 |
+
匹配要求:
|
172 |
+
简历中必须明确列出与目标职位直接相关的核心技能或关键词(建议不少于3项,最多5项计分)。
|
173 |
+
评分细则:
|
174 |
+
每列出一项与目标职位高度匹配的技能或关键词,得6分(最多计5项得分)。
|
175 |
+
如未列出任何相关技能或关键词,直接扣30分。
|
176 |
+
若关键词存在但与目标职位匹配度较低或描述不清晰,依据实际情况酌情扣分(每项扣分范围为2-4分)。
|
177 |
+
【新增】岗位相关关键词匹配度额外评分:
|
178 |
+
• 完全匹配目标岗位核心技能:每项+2分(最高+10分)
|
179 |
+
• 部分匹配目标岗位次要技能:每项+1分(最高+5分)
|
180 |
+
|
181 |
+
四、工作成就与项目描述(满分20分,必须遵循STAR法则)
|
182 |
+
要求说明:
|
183 |
+
每段工作经历或项目描述必须完整包含:
|
184 |
+
Situation(情境): 说明工作/项目背景与挑战。
|
185 |
+
Task(任务): 说明你在该情境下需要完成的任务。
|
186 |
+
Action(行动): 描述为解决问题所采取的具体措施。
|
187 |
+
Result(结果): 列出取得的成果和影响(最好附量化指标)。
|
188 |
+
评分细则:
|
189 |
+
每完整描述一项工作或项目经历且具备STAR所有要素:得5分,最多计4项得分。
|
190 |
+
若工作或项目描述存在缺失或不清晰(例如缺少关键STAR元素),则每项扣2-5分(依据缺失程度和信息模糊程度)。
|
191 |
+
如果简历完全没有相关描述,直接扣20分。
|
192 |
+
【新增】岗位相关项目经验额外评分:
|
193 |
+
• 高度相关项目:每项+3分(最高+9分)
|
194 |
+
• 部分相关项目:每项+1分(最高+3分)
|
195 |
+
|
196 |
+
五、语言表达及排版质量(满分10分)
|
197 |
+
表达与排版要求:
|
198 |
+
整体语言表达准确、专业,无明显错别字或语法错误。
|
199 |
+
排版整洁、格式统一,避免混乱或信息堆砌。
|
200 |
+
评分细则:
|
201 |
+
排版格式符合要求,得5分;若出现明显格式错误或杂乱,每项错误扣1至5分,累计扣分最高5分。
|
202 |
+
语言表达无错别字或语法错误,得5分;每出现一处错别字或语法错误扣1分(最多扣5分)。
|
203 |
+
|
204 |
+
简历内容:
|
205 |
+
{text[:30000]}{'...' if len(text) > 30000 else ''}"""
|
206 |
+
|
207 |
+
try:
|
208 |
+
response = client.chat.completions.create(
|
209 |
+
model="deepseek-chat",
|
210 |
+
messages=[
|
211 |
+
{"role": "system", "content": "你是一位严格的简历评估专家。你必须严格按照评分标准进行评分和扣分,并明确指出每个缺点在简历中的具体位置。总分为100分,最终分数 = 100 - 总扣分。"},
|
212 |
+
{"role": "user", "content": prompt}
|
213 |
+
],
|
214 |
+
temperature=1,
|
215 |
+
stream=False
|
216 |
+
)
|
217 |
+
|
218 |
+
content = response.choices[0].message.content
|
219 |
+
|
220 |
+
# 输出原始 AI 响应以便调试
|
221 |
+
print("======== RAW AI RESPONSE ========")
|
222 |
+
print(content)
|
223 |
+
print("=================================")
|
224 |
+
|
225 |
+
# 解析响应内容
|
226 |
+
lines = [line.strip() for line in content.split('\n') if line.strip()]
|
227 |
+
suggestions = []
|
228 |
+
deduction_points = 0
|
229 |
+
current_section = None
|
230 |
+
|
231 |
+
# 首先查找显式的总扣分声明
|
232 |
+
total_deduction_match = None
|
233 |
+
for line in lines:
|
234 |
+
total_deduction_match = re.search(r'总扣分[::]\s*(\d+)分', line)
|
235 |
+
if total_deduction_match:
|
236 |
+
deduction_points = int(total_deduction_match.group(1))
|
237 |
+
break
|
238 |
+
|
239 |
+
# 如果没有找到显式总扣分,则尝试从缺点部分累加
|
240 |
+
if total_deduction_match is None:
|
241 |
+
in_cons_section = False
|
242 |
+
for line in lines:
|
243 |
+
if re.match(r'^\[?缺点\]?', line, re.IGNORECASE):
|
244 |
+
in_cons_section = True
|
245 |
+
continue
|
246 |
+
if in_cons_section:
|
247 |
+
deduction_match = re.search(r'\(-(\d+)分\)', line)
|
248 |
+
if deduction_match:
|
249 |
+
deduction_points += int(deduction_match.group(1))
|
250 |
+
|
251 |
+
# 确保扣分值在合理范围内
|
252 |
+
deduction_points = max(0, min(100, deduction_points))
|
253 |
+
|
254 |
+
# 计算最终分数
|
255 |
+
score = max(0, min(100, 100 - deduction_points))
|
256 |
+
current_section = None
|
257 |
+
|
258 |
+
# 更严格的章节检测
|
259 |
+
for line in lines:
|
260 |
+
# 检测章节标题
|
261 |
+
if re.match(r'^\[?整体总结\]?', line, re.IGNORECASE):
|
262 |
+
current_section = "summary"
|
263 |
+
suggestions.append(line)
|
264 |
+
continue
|
265 |
+
elif re.match(r'^\[?优点\]?', line, re.IGNORECASE):
|
266 |
+
current_section = "pros"
|
267 |
+
suggestions.append(line)
|
268 |
+
continue
|
269 |
+
elif re.match(r'^\[?缺点\]?', line, re.IGNORECASE):
|
270 |
+
current_section = "cons"
|
271 |
+
suggestions.append(line)
|
272 |
+
continue
|
273 |
+
elif re.match(r'^\[?扣分项\]?', line, re.IGNORECASE):
|
274 |
+
current_section = "deduction"
|
275 |
+
continue
|
276 |
+
|
277 |
+
# 只保留当前章节的内容
|
278 |
+
if current_section in ["summary", "pros", "cons"]:
|
279 |
+
suggestions.append(line)
|
280 |
+
|
281 |
+
return suggestions, score
|
282 |
+
|
283 |
+
except Exception as e:
|
284 |
+
print(f"AI分析错误: {str(e)}")
|
285 |
+
return [f"AI分析时发生错误: {str(e)}"], 0
|
286 |
+
|
287 |
+
@app.route('/')
|
288 |
+
def index():
|
289 |
+
return render_template('index.html')
|
290 |
+
|
291 |
+
@app.route('/upload', methods=['POST'])
|
292 |
+
def upload_file():
|
293 |
+
if 'resume' not in request.files:
|
294 |
+
return jsonify({'error': '请选择文件上传'}), 400
|
295 |
+
|
296 |
+
file = request.files['resume']
|
297 |
+
if file.filename == '':
|
298 |
+
return jsonify({'error': '未选择文件'}), 400
|
299 |
+
|
300 |
+
try:
|
301 |
+
# 从form获取job_position
|
302 |
+
job_position = request.form.get('job_position')
|
303 |
+
if not job_position:
|
304 |
+
return jsonify({'error': '请选择目标岗位'}), 400
|
305 |
+
|
306 |
+
filename = safe_filename(file.filename)
|
307 |
+
file_stream = BytesIO(file.read())
|
308 |
+
|
309 |
+
text = extract_text_from_file(file_stream, file.filename)
|
310 |
+
if not text.strip():
|
311 |
+
return jsonify({'error': '文件内容为空或无法解析'}), 400
|
312 |
+
|
313 |
+
suggestions, score = analyze_resume_with_ai(text, job_position)
|
314 |
+
|
315 |
+
return jsonify({
|
316 |
+
'message': '分析成功',
|
317 |
+
'suggestions': suggestions,
|
318 |
+
'score': score,
|
319 |
+
'filename': filename,
|
320 |
+
'job_position': job_position
|
321 |
+
})
|
322 |
+
except ValueError as e:
|
323 |
+
return jsonify({'error': str(e)}), 400
|
324 |
+
except Exception as e:
|
325 |
+
print(f"上传处理错误: {str(e)}")
|
326 |
+
return jsonify({'error': f'处理失败: {str(e)}'}), 500
|
327 |
+
|
328 |
+
@app.route('/generate_cover_letter', methods=['POST'])
|
329 |
+
def generate_cover_letter():
|
330 |
+
try:
|
331 |
+
data = request.json
|
332 |
+
required_fields = ['company_name', 'position', 'resume_text', 'job_description']
|
333 |
+
|
334 |
+
# 验证必填字段
|
335 |
+
for field in required_fields:
|
336 |
+
if not data.get(field):
|
337 |
+
return jsonify({'error': f'缺少必填字段: {field}'}), 400
|
338 |
+
|
339 |
+
# 构建AI提示词 - 优化版
|
340 |
+
prompt = f"""你是一位专业的职业顾问,需要根据申请人提供的简历内容撰写求职信。请严格遵守以下规则:
|
341 |
+
|
342 |
+
1. 信息真实性原则:
|
343 |
+
- 只能使用简历中明确列出的教育背景、工作经历、项目经验和技能
|
344 |
+
- 绝对禁止添加、编造或推断简历中没有的信息
|
345 |
+
- 如果某项要求(如特定技能)在简历中未体现,不要在求职信中提及
|
346 |
+
|
347 |
+
2. 内容要求:
|
348 |
+
[必须包含的格式要素]
|
349 |
+
- 正式商务信函格式(日期、称呼、正文、结尾敬语)
|
350 |
+
- 称呼使用"尊敬的招聘经理"(如不知道具体姓名)
|
351 |
+
- 结尾要有明确的行动号召(如期待面试机会)
|
352 |
+
|
353 |
+
[内容结构]
|
354 |
+
第一段:明确申请职位和动机(30-50字)
|
355 |
+
第二段:从简历中提取与职位最相关的2-3个核心优势(80-120字)
|
356 |
+
第三段:结合公司文化和职位要求的具体匹配点(80-120字)
|
357 |
+
第四段:礼貌结尾和行动号召(30-50字)
|
358 |
+
|
359 |
+
3. 写作规范:
|
360 |
+
- 语言简洁专业,总字数严格控制在300-400字
|
361 |
+
- 使用主动语态和积极措辞
|
362 |
+
- 量化成果时只能使用简历中提供的数据
|
363 |
+
- 避免使用夸张或主观的描述词
|
364 |
+
|
365 |
+
4. 特别注意:
|
366 |
+
- 如果简历中没有公司要求的关键技能或经验,不要在信中编造
|
367 |
+
- 不要假设任何简历中没有的工作职责或成就
|
368 |
+
- 不要添加简历中未列出的证书、奖项或培训经历
|
369 |
+
|
370 |
+
[申请人简历内容]
|
371 |
+
{data['resume_text'][:10000]}
|
372 |
+
|
373 |
+
[目标公司信息]
|
374 |
+
公司名称: {data['company_name']}
|
375 |
+
公司介绍: {data.get('company_info', '未提供')}
|
376 |
+
|
377 |
+
[申请职位]
|
378 |
+
{data['position']}
|
379 |
+
|
380 |
+
[职位描述及要求]
|
381 |
+
{data['job_description']}
|
382 |
+
|
383 |
+
[申请动机]
|
384 |
+
{data.get('motivation', '未提供')}
|
385 |
+
|
386 |
+
请现在开始撰写求职信,严格遵循以上所有要求。"""
|
387 |
+
|
388 |
+
# 调用AI生成推荐信
|
389 |
+
response = client.chat.completions.create(
|
390 |
+
model="deepseek-chat",
|
391 |
+
messages=[
|
392 |
+
{
|
393 |
+
"role": "system",
|
394 |
+
"content": """你是一位严谨的职业顾问,专门帮助求职者撰写基于事实的求职信。
|
395 |
+
你必须:
|
396 |
+
1. 只使用申请人简历中明确提供的信息
|
397 |
+
2. 绝不添加、推断或编造任何简历中没有的内容
|
398 |
+
3. 如果简历缺少职位要求的关键资质,如实呈现而不虚构
|
399 |
+
4. 所有成就描述必须有简历中的具体数据支持"""
|
400 |
+
},
|
401 |
+
{"role": "user", "content": prompt}
|
402 |
+
],
|
403 |
+
temperature=0.5, # 降低创造性,提高准确性
|
404 |
+
max_tokens=2000
|
405 |
+
)
|
406 |
+
|
407 |
+
content = response.choices[0].message.content
|
408 |
+
|
409 |
+
# 后处理检查
|
410 |
+
if "简历中未提及" in content or "根据我的了解" in content:
|
411 |
+
raise ValueError("AI尝试添加简历外信息")
|
412 |
+
|
413 |
+
return jsonify({
|
414 |
+
'success': True,
|
415 |
+
'cover_letter': content,
|
416 |
+
'word_count': len(content.split())
|
417 |
+
})
|
418 |
+
|
419 |
+
except Exception as e:
|
420 |
+
print(f"推荐信生成错误: {str(e)}")
|
421 |
+
return jsonify({'error': f'生成失败: {str(e)}'}), 500
|
422 |
+
|
423 |
+
if __name__ == '__main__':
|
424 |
+
app.run(debug=True)
|