Spaces:
Running
Running
fix: bugs around debug mode
Browse files- .gitignore +3 -2
- app.py +8 -6
- config.py +1 -1
- taskAI.py +10 -7
- taskNonAI.py +8 -0
- util.py +4 -4
.gitignore
CHANGED
@@ -1,7 +1,8 @@
|
|
1 |
*_secret.py
|
2 |
*_secret.py*
|
3 |
-
|
4 |
-
|
|
|
5 |
|
6 |
.local/
|
7 |
.ruff_cache/
|
|
|
1 |
*_secret.py
|
2 |
*_secret.py*
|
3 |
+
**/test_result.pdf
|
4 |
+
**/letter.typ
|
5 |
+
**/cover_letter_*.typ
|
6 |
|
7 |
.local/
|
8 |
.ruff_cache/
|
app.py
CHANGED
@@ -6,6 +6,7 @@ from util import mylogger
|
|
6 |
from util import stream_together
|
7 |
from util import checkAPI
|
8 |
from taskNonAI import extract_url, file_to_html, compile_pdf
|
|
|
9 |
|
10 |
## load data
|
11 |
from _data_test import mock_jd, mock_cv
|
@@ -32,7 +33,7 @@ def init():
|
|
32 |
## Config Functions
|
33 |
|
34 |
|
35 |
-
def set_same_cheap_strong(set_same: bool, cheap_base, cheap_key):
|
36 |
# setup_zone = gr.Accordion("AI setup (OpenAI-compatible LLM API)", open=True)
|
37 |
if set_same:
|
38 |
return (
|
@@ -92,9 +93,9 @@ def run_refine(api_base, api_key, api_model, jd_info, cv_text):
|
|
92 |
yield result
|
93 |
|
94 |
|
95 |
-
def run_compose(api_base, api_key, api_model, min_jd, min_cv):
|
96 |
strongAPI = {"base": api_base, "key": api_key, "model": api_model}
|
97 |
-
taskAI = TaskAI(strongAPI, temperature=0.6, max_tokens=4000)
|
98 |
info("Composing letter with CoT ...")
|
99 |
result = ""
|
100 |
for response in taskAI.compose_letter_CoT(jd=min_jd, resume=min_cv):
|
@@ -112,7 +113,7 @@ def finalize_letter_txt(api_base, api_key, api_model, debug_CoT):
|
|
112 |
yield result
|
113 |
|
114 |
|
115 |
-
def finalize_letter_pdf(api_base, api_key, api_model, jd, cv, cover_letter_text):
|
116 |
cheapAPI = {"base": api_base, "key": api_key, "model": api_model}
|
117 |
taskAI = TaskAI(cheapAPI, temperature=0.1, max_tokens=100)
|
118 |
meta_data = next(taskAI.get_jobapp_meta(JD=jd, CV=cv))
|
@@ -122,12 +123,13 @@ def finalize_letter_pdf(api_base, api_key, api_model, jd, cv, cover_letter_text)
|
|
122 |
pdf_context,
|
123 |
tmpl_path="typst/template_letter.tmpl",
|
124 |
output_path=f"/tmp/cover_letter_by_{pdf_context['applicantFullName']}_to_{pdf_context['companyFullName']}.pdf",
|
|
|
125 |
)
|
126 |
|
127 |
|
128 |
with gr.Blocks(
|
129 |
title=DEMO_TITLE,
|
130 |
-
theme=gr.themes.Soft(primary_hue="
|
131 |
) as app:
|
132 |
intro = f"""# {DEMO_TITLE}
|
133 |
> You provide job description and résumé. I write Cover letter for you!
|
@@ -226,7 +228,7 @@ with gr.Blocks(
|
|
226 |
outputs=[expert_zone, reformat_zone],
|
227 |
).success(
|
228 |
fn=run_compose,
|
229 |
-
inputs=[strong_base, strong_key, strong_model, min_jd, min_cv],
|
230 |
outputs=[debug_CoT],
|
231 |
).success(
|
232 |
fn=lambda: gr.Accordion("Expert Zone", open=False),
|
|
|
6 |
from util import stream_together
|
7 |
from util import checkAPI
|
8 |
from taskNonAI import extract_url, file_to_html, compile_pdf
|
9 |
+
from taskAI import TaskAI
|
10 |
|
11 |
## load data
|
12 |
from _data_test import mock_jd, mock_cv
|
|
|
33 |
## Config Functions
|
34 |
|
35 |
|
36 |
+
def set_same_cheap_strong(set_same: bool, cheap_base, cheap_key, cheap_model):
|
37 |
# setup_zone = gr.Accordion("AI setup (OpenAI-compatible LLM API)", open=True)
|
38 |
if set_same:
|
39 |
return (
|
|
|
93 |
yield result
|
94 |
|
95 |
|
96 |
+
def run_compose(api_base, api_key, api_model, min_jd, min_cv, is_debug):
|
97 |
strongAPI = {"base": api_base, "key": api_key, "model": api_model}
|
98 |
+
taskAI = TaskAI(strongAPI, is_debug=is_debug, temperature=0.6, max_tokens=4000)
|
99 |
info("Composing letter with CoT ...")
|
100 |
result = ""
|
101 |
for response in taskAI.compose_letter_CoT(jd=min_jd, resume=min_cv):
|
|
|
113 |
yield result
|
114 |
|
115 |
|
116 |
+
def finalize_letter_pdf(api_base, api_key, api_model, jd, cv, cover_letter_text, is_debug):
|
117 |
cheapAPI = {"base": api_base, "key": api_key, "model": api_model}
|
118 |
taskAI = TaskAI(cheapAPI, temperature=0.1, max_tokens=100)
|
119 |
meta_data = next(taskAI.get_jobapp_meta(JD=jd, CV=cv))
|
|
|
123 |
pdf_context,
|
124 |
tmpl_path="typst/template_letter.tmpl",
|
125 |
output_path=f"/tmp/cover_letter_by_{pdf_context['applicantFullName']}_to_{pdf_context['companyFullName']}.pdf",
|
126 |
+
is_debug=is_debug,
|
127 |
)
|
128 |
|
129 |
|
130 |
with gr.Blocks(
|
131 |
title=DEMO_TITLE,
|
132 |
+
theme=gr.themes.Soft(primary_hue="sky", secondary_hue="emerald", neutral_hue="stone"),
|
133 |
) as app:
|
134 |
intro = f"""# {DEMO_TITLE}
|
135 |
> You provide job description and résumé. I write Cover letter for you!
|
|
|
228 |
outputs=[expert_zone, reformat_zone],
|
229 |
).success(
|
230 |
fn=run_compose,
|
231 |
+
inputs=[strong_base, strong_key, strong_model, min_jd, min_cv, is_debug],
|
232 |
outputs=[debug_CoT],
|
233 |
).success(
|
234 |
fn=lambda: gr.Accordion("Expert Zone", open=False),
|
config.py
CHANGED
@@ -14,7 +14,7 @@ STRONG_MODEL = os.getenv("STRONG_MODEL") or "gpt-4"
|
|
14 |
IS_SHARE = bool(os.getenv("IS_SHARE")) or False
|
15 |
IS_DEBUG = bool(os.getenv("IS_DEBUG")) or False
|
16 |
|
17 |
-
DEMO_TITLE = "Cover Letter Generator"
|
18 |
DEMO_DESCRIPTION = "This is a demo of the OpenAI API for generating cover letters. The model is trained on a dataset of cover letters and job descriptions, and generates a cover letter based on the job description and the applicant's CV. The model is fine-tuned on the OpenAI API, and is able to generate cover letters that are tailored to the job description and the applicant's CV. The model is able to generate cover letters for a wide range of jobs, and is able to generate cover letters that are tailored to the job description and the applicant's CV. The model is able to generate cover letters for a wide range of jobs, and is able to generate cover letters that are tailored to the job description and the applicant's CV. The model is able to generate cover letters for a wide range of jobs, and is able to generate cover letters that are tailored to the job description and the applicant's CV."
|
19 |
|
20 |
CV_EXT = [".typ", ".tex", ".html", ".docx", ".rst", ".rtf", ".odt", ".txt", ".md"]
|
|
|
14 |
IS_SHARE = bool(os.getenv("IS_SHARE")) or False
|
15 |
IS_DEBUG = bool(os.getenv("IS_DEBUG")) or False
|
16 |
|
17 |
+
DEMO_TITLE = "CoverPilot: AI-Powered Cover Letter Generator"
|
18 |
DEMO_DESCRIPTION = "This is a demo of the OpenAI API for generating cover letters. The model is trained on a dataset of cover letters and job descriptions, and generates a cover letter based on the job description and the applicant's CV. The model is fine-tuned on the OpenAI API, and is able to generate cover letters that are tailored to the job description and the applicant's CV. The model is able to generate cover letters for a wide range of jobs, and is able to generate cover letters that are tailored to the job description and the applicant's CV. The model is able to generate cover letters for a wide range of jobs, and is able to generate cover letters that are tailored to the job description and the applicant's CV. The model is able to generate cover letters for a wide range of jobs, and is able to generate cover letters that are tailored to the job description and the applicant's CV."
|
19 |
|
20 |
CV_EXT = [".typ", ".tex", ".html", ".docx", ".rst", ".rtf", ".odt", ".txt", ".md"]
|
taskAI.py
CHANGED
@@ -37,7 +37,7 @@ JSON_API = ChatPromptTemplate(
|
|
37 |
[
|
38 |
ChatMessage(
|
39 |
role="system",
|
40 |
-
content="You are a JSON API. Your mission is to convert user input into a JSON object
|
41 |
),
|
42 |
ChatMessage(role="user", content="{content}"),
|
43 |
]
|
@@ -65,7 +65,8 @@ Before officially write the letter, think step by step. First, list what makes a
|
|
65 |
|
66 |
## tasks
|
67 |
class TaskAI(OpenAILike):
|
68 |
-
|
|
|
69 |
log = logger.info
|
70 |
|
71 |
def guess_window_size(model=api["model"]):
|
@@ -82,8 +83,8 @@ class TaskAI(OpenAILike):
|
|
82 |
log(f"use context window size: {window_size} for {model}")
|
83 |
return window_size
|
84 |
|
85 |
-
checkAPI(api_base, api_key)
|
86 |
-
|
87 |
|
88 |
super().__init__(
|
89 |
api_base=api["base"],
|
@@ -93,6 +94,7 @@ class TaskAI(OpenAILike):
|
|
93 |
context_window=guess_window_size(),
|
94 |
**kwargs,
|
95 |
)
|
|
|
96 |
|
97 |
def _debug_print_msg(self, msg):
|
98 |
if not self.is_debug:
|
@@ -114,7 +116,7 @@ class TaskAI(OpenAILike):
|
|
114 |
|
115 |
def compose_letter_CoT(self, resume: str, jd: str):
|
116 |
msg = LETTER_COMPOSE.format_messages(resume=resume, jd=jd)
|
117 |
-
_debug_print_msg(msg)
|
118 |
return self.stream_chat(msg)
|
119 |
|
120 |
def get_jobapp_meta(self, JD, CV):
|
@@ -136,7 +138,8 @@ class TaskAI(OpenAILike):
|
|
136 |
try:
|
137 |
meta_JD = json.loads(meta_JD.strip())
|
138 |
meta_CV = json.loads(meta_CV.strip())
|
139 |
-
except:
|
|
|
140 |
raise ValueError(
|
141 |
f"AI didn't return a valid JSON string. Try again or consider a better model for CheapAI. \n{meta_JD}\n{meta_CV}"
|
142 |
)
|
@@ -148,7 +151,7 @@ class TaskAI(OpenAILike):
|
|
148 |
def purify_letter(self, full_text):
|
149 |
return self.stream_chat(
|
150 |
EXTRACT_INFO.format_messages(
|
151 |
-
to_extract="the cover letter section starting from 'Dear Hiring Manager' or similar to 'Sincerely,' or similar
|
152 |
input=full_text,
|
153 |
)
|
154 |
)
|
|
|
37 |
[
|
38 |
ChatMessage(
|
39 |
role="system",
|
40 |
+
content="You are a JSON API. Your mission is to convert user input into a valid and complete JSON object STRICTLY in this template: {template}. The output should be completely a plain json without nested structure. Never summerize, paraphrase or do anything else, just extract the information from the input and fill in the template.",
|
41 |
),
|
42 |
ChatMessage(role="user", content="{content}"),
|
43 |
]
|
|
|
65 |
|
66 |
## tasks
|
67 |
class TaskAI(OpenAILike):
|
68 |
+
is_debug = False
|
69 |
+
def __init__(self, api: dict[str, str], is_debug=False, **kwargs):
|
70 |
log = logger.info
|
71 |
|
72 |
def guess_window_size(model=api["model"]):
|
|
|
83 |
log(f"use context window size: {window_size} for {model}")
|
84 |
return window_size
|
85 |
|
86 |
+
checkAPI(api_base=api["base"], api_key=api["key"])
|
87 |
+
|
88 |
|
89 |
super().__init__(
|
90 |
api_base=api["base"],
|
|
|
94 |
context_window=guess_window_size(),
|
95 |
**kwargs,
|
96 |
)
|
97 |
+
self.is_debug = is_debug
|
98 |
|
99 |
def _debug_print_msg(self, msg):
|
100 |
if not self.is_debug:
|
|
|
116 |
|
117 |
def compose_letter_CoT(self, resume: str, jd: str):
|
118 |
msg = LETTER_COMPOSE.format_messages(resume=resume, jd=jd)
|
119 |
+
self._debug_print_msg(msg)
|
120 |
return self.stream_chat(msg)
|
121 |
|
122 |
def get_jobapp_meta(self, JD, CV):
|
|
|
138 |
try:
|
139 |
meta_JD = json.loads(meta_JD.strip())
|
140 |
meta_CV = json.loads(meta_CV.strip())
|
141 |
+
except Exception as e:
|
142 |
+
print(e)
|
143 |
raise ValueError(
|
144 |
f"AI didn't return a valid JSON string. Try again or consider a better model for CheapAI. \n{meta_JD}\n{meta_CV}"
|
145 |
)
|
|
|
151 |
def purify_letter(self, full_text):
|
152 |
return self.stream_chat(
|
153 |
EXTRACT_INFO.format_messages(
|
154 |
+
to_extract="the cover letter section starting from 'Dear Hiring Manager' or similar to 'Sincerely,' or similar",
|
155 |
input=full_text,
|
156 |
)
|
157 |
)
|
taskNonAI.py
CHANGED
@@ -52,6 +52,12 @@ def _date() -> str:
|
|
52 |
def _typst_escape(s) -> str:
|
53 |
return str(s).replace("@", "\@").replace("#", "\#")
|
54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
|
56 |
def compile_pdf(
|
57 |
context: dict, tmpl_path: str, output_path="/tmp/cover_letter.pdf", is_debug=False
|
@@ -62,6 +68,8 @@ def compile_pdf(
|
|
62 |
tmpl = Template(f.read())
|
63 |
context = {k: _typst_escape(v) for k, v in context.items()}
|
64 |
context.update({"date_string": _date()})
|
|
|
|
|
65 |
letter_typ = tmpl.safe_substitute(context)
|
66 |
with open(letter_src_filepath, "w", encoding="utf8") as f:
|
67 |
f.write(letter_typ)
|
|
|
52 |
def _typst_escape(s) -> str:
|
53 |
return str(s).replace("@", "\@").replace("#", "\#")
|
54 |
|
55 |
+
def _ensure_no_signature_in_body(cover_letter_body: str) -> str:
|
56 |
+
if not cover_letter_body.strip().endswith(","):
|
57 |
+
# remove last line
|
58 |
+
cover_letter_body = "\n".join(cover_letter_body.split("\n")[:-1])
|
59 |
+
print(cover_letter_body)
|
60 |
+
return cover_letter_body
|
61 |
|
62 |
def compile_pdf(
|
63 |
context: dict, tmpl_path: str, output_path="/tmp/cover_letter.pdf", is_debug=False
|
|
|
68 |
tmpl = Template(f.read())
|
69 |
context = {k: _typst_escape(v) for k, v in context.items()}
|
70 |
context.update({"date_string": _date()})
|
71 |
+
context["letter_body"]=_ensure_no_signature_in_body(context["letter_body"])
|
72 |
+
|
73 |
letter_typ = tmpl.safe_substitute(context)
|
74 |
with open(letter_src_filepath, "w", encoding="utf8") as f:
|
75 |
f.write(letter_typ)
|
util.py
CHANGED
@@ -36,16 +36,16 @@ def is_valid_url(url: str) -> bool:
|
|
36 |
|
37 |
def is_valid_openai_api_key(api_base: str, api_key: str) -> bool:
|
38 |
headers = {"Authorization": f"Bearer {api_key}"}
|
39 |
-
|
40 |
-
response = requests.get(
|
41 |
-
|
42 |
return response.status_code == 200
|
43 |
|
44 |
|
45 |
def checkAPI(api_base: str, api_key: str):
|
46 |
if not is_valid_openai_api_key(api_base, api_key):
|
47 |
raise ValueError(
|
48 |
-
"
|
49 |
)
|
50 |
|
51 |
|
|
|
36 |
|
37 |
def is_valid_openai_api_key(api_base: str, api_key: str) -> bool:
|
38 |
headers = {"Authorization": f"Bearer {api_key}"}
|
39 |
+
test_url = f"{api_base}/models"
|
40 |
+
response = requests.get(test_url, headers=headers)
|
41 |
+
print(response.json())
|
42 |
return response.status_code == 200
|
43 |
|
44 |
|
45 |
def checkAPI(api_base: str, api_key: str):
|
46 |
if not is_valid_openai_api_key(api_base, api_key):
|
47 |
raise ValueError(
|
48 |
+
"API not available. Maybe it's OpenAI's (or AI provider's) fault, or you setup your AI APIs icorrectly. If you don't have any API key, try get one from https://beta.openai.com/account/api-keys"
|
49 |
)
|
50 |
|
51 |
|