cxumol commited on
Commit
9087846
·
1 Parent(s): a86d269
Files changed (5) hide show
  1. _secret.template.py +2 -6
  2. app.py +93 -44
  3. taskAI.py +1 -1
  4. taskNonAI.py +24 -12
  5. util.py +6 -2
_secret.template.py CHANGED
@@ -1,11 +1,7 @@
1
  from util import zip_api
2
 
3
- api_test = zip_api(
4
- api_base="",
5
- api_key="",
6
- model=""
7
- )
8
 
9
  cmd = """
10
  OPENAI_API_BASE="" OPENAI_API_KEY="" CHEAP_MODEL="" STRONG_MODEL="" run.sh
11
- """
 
1
  from util import zip_api
2
 
3
+ api_test = zip_api(api_base="", api_key="", model="")
 
 
 
 
4
 
5
  cmd = """
6
  OPENAI_API_BASE="" OPENAI_API_KEY="" CHEAP_MODEL="" STRONG_MODEL="" run.sh
7
+ """
app.py CHANGED
@@ -9,17 +9,21 @@ from taskNonAI import extract_url, file_to_html, compile_pdf
9
 
10
  ## load data
11
  from _data_test import mock_jd, mock_cv
 
12
  ## ui
13
  import gradio as gr
 
14
  ## dependency
15
  from pypandoc.pandoc_download import download_pandoc
 
16
  ## std
17
  import os
18
  import json
19
 
20
- logger = mylogger(__name__,'%(asctime)s:%(levelname)s:%(message)s')
21
  info = logger.info
22
 
 
23
  def init():
24
  os.system("shot-scraper install -b firefox")
25
  download_pandoc()
@@ -27,24 +31,32 @@ def init():
27
 
28
  ## Config Functions
29
 
30
- def set_same_cheap_strong(set_same:bool, cheap_base, cheap_key):
 
31
  # setup_zone = gr.Accordion("AI setup (OpenAI-compatible LLM API)", open=True)
32
  if set_same:
33
- return (gr.Textbox(value=cheap_base, label="API Base", interactive=False),
34
- gr.Textbox(value=cheap_key, label="API key", type="password", interactive=False),
35
- gr.Textbox(value=cheap_model, label="Model ID", interactive=False),
36
- # setup_zone,
37
- )
 
 
 
38
  else:
39
- return (gr.Textbox(value=cheap_base, label="API Base", interactive=True),
40
- gr.Textbox(value=cheap_key, label="API key", type="password", interactive=True),
41
- gr.Textbox(value=cheap_model, label="Model ID", interactive=True),
42
- # setup_zone,
43
- )
44
-
 
 
 
45
 
46
  ## Main Functions
47
 
 
48
  def prepare_input(jd_info, cv_file: str, cv_text):
49
  if jd_info:
50
  if is_valid_url(jd_info):
@@ -66,8 +78,9 @@ def prepare_input(jd_info, cv_file: str, cv_text):
66
  cv = mock_cv
67
  return jd, cv
68
 
 
69
  def run_refine(api_base, api_key, api_model, jd_info, cv_text):
70
- jd,cv=jd_info,cv_text
71
  cheapAPI = {"base": api_base, "key": api_key, "model": api_model}
72
  taskAI = TaskAI(cheapAPI, temperature=0.2, max_tokens=2048) # max_tokens=2048
73
  info("API initialized")
@@ -78,6 +91,7 @@ def run_refine(api_base, api_key, api_model, jd_info, cv_text):
78
  for result in gen:
79
  yield result
80
 
 
81
  def run_compose(api_base, api_key, api_model, min_jd, min_cv):
82
  strongAPI = {"base": api_base, "key": api_key, "model": api_model}
83
  taskAI = TaskAI(strongAPI, temperature=0.6, max_tokens=4000)
@@ -87,22 +101,29 @@ def run_compose(api_base, api_key, api_model, min_jd, min_cv):
87
  result += response.delta
88
  yield result
89
 
 
90
  def finalize_letter_txt(api_base, api_key, api_model, debug_CoT):
91
  cheapAPI = {"base": api_base, "key": api_key, "model": api_model}
92
  taskAI = TaskAI(cheapAPI, temperature=0.2, max_tokens=2048)
93
  info("Finalizing letter ...")
94
- result=""
95
  for response in taskAI.purify_letter(full_text=debug_CoT):
96
  result += response.delta
97
  yield result
98
 
 
99
  def finalize_letter_pdf(api_base, api_key, api_model, jd, cv, cover_letter_text):
100
  cheapAPI = {"base": api_base, "key": api_key, "model": api_model}
101
  taskAI = TaskAI(cheapAPI, temperature=0.1, max_tokens=100)
102
  meta_data = next(taskAI.get_jobapp_meta(JD=jd, CV=cv))
103
  pdf_context = json.loads(meta_data)
104
  pdf_context["letter_body"] = cover_letter_text
105
- return meta_data, compile_pdf(pdf_context,tmpl_path="typst/template_letter.tmpl",output_path=f"/tmp/cover_letter_by_{pdf_context['applicantFullName']}_to_{pdf_context['companyFullName']}.pdf")
 
 
 
 
 
106
 
107
  with gr.Blocks(
108
  title=DEMO_TITLE,
@@ -116,26 +137,28 @@ with gr.Blocks(
116
 
117
  with gr.Row():
118
  with gr.Column(scale=1):
119
- with gr.Accordion("AI setup (OpenAI-compatible LLM API)", open=False) as setup_zone:
120
- is_debug = gr.Checkbox( label="Debug Mode", value=IS_DEBUG)
121
-
 
 
122
  gr.Markdown(
123
  "**Cheap AI**, an honest format converter and refiner, extracts essential info from job description and résumé, to reduce subsequent cost on Strong AI."
124
  )
125
  with gr.Group():
126
- cheap_base = gr.Textbox(
127
- value=CHEAP_API_BASE, label="API Base"
 
128
  )
129
- cheap_key = gr.Textbox(value=CHEAP_API_KEY, label="API key", type="password")
130
  cheap_model = gr.Textbox(value=CHEAP_MODEL, label="Model ID")
131
  gr.Markdown(
132
  "---\n**Strong AI**, a thoughtful wordsmith, generates perfect cover letters to make both you and recruiters happy."
133
  )
134
- is_same_cheap_strong = gr.Checkbox(label="the same as Cheap AI", value=False, container=False)
 
 
135
  with gr.Group():
136
- strong_base = gr.Textbox(
137
- value=STRONG_API_BASE, label="API Base"
138
- )
139
  strong_key = gr.Textbox(
140
  value=STRONG_API_KEY, label="API key", type="password"
141
  )
@@ -150,7 +173,7 @@ with gr.Blocks(
150
  )
151
  with gr.Group():
152
  gr.Markdown("## Applicant - CV / Résumé")
153
- # with gr.Row():
154
  cv_file = gr.File(
155
  label="Allowed formats: " + " ".join(CV_EXT),
156
  file_count="single",
@@ -168,7 +191,6 @@ with gr.Blocks(
168
  min_jd = gr.TextArea(label="Reformatted Job Description")
169
  min_cv = gr.TextArea(label="Reformatted CV / Résumé")
170
  with gr.Accordion("Expert Zone", open=False) as expert_zone:
171
-
172
  debug_CoT = gr.Textbox(label="Chain of Thoughts")
173
  debug_jobapp = gr.Textbox(label="Job application meta data")
174
  cover_letter_text = gr.Textbox(label="Cover Letter")
@@ -179,27 +201,54 @@ with gr.Blocks(
179
  type="filepath",
180
  )
181
  infer_btn = gr.Button("Go!", variant="primary")
182
-
183
- is_same_cheap_strong.change(fn= set_same_cheap_strong,
184
- inputs=[is_same_cheap_strong, cheap_base, cheap_key, cheap_model],
185
- outputs=[strong_base, strong_key, strong_model])
186
-
187
- infer_btn.click(fn= set_same_cheap_strong,
188
- inputs=[is_same_cheap_strong, cheap_base, cheap_key, cheap_model],
189
- outputs=[strong_base, strong_key, strong_model]
 
 
 
190
  ).success(
191
- fn=prepare_input,
192
- inputs=[jd_info, cv_file, cv_text],
193
- outputs=[jd_info, cv_text]
194
  ).success(
195
  fn=run_refine,
196
  inputs=[cheap_base, cheap_key, cheap_model, jd_info, cv_text],
197
  outputs=[min_jd, min_cv],
198
- ).success(fn=lambda:[gr.Accordion("Expert Zone", open=True),gr.Accordion("Reformatting", open=False)],inputs=None, outputs=[expert_zone, reformat_zone]
199
- ).success(fn=run_compose, inputs=[strong_base, strong_key, strong_model, min_jd, min_cv], outputs=[debug_CoT]
200
- ).success(fn=lambda:gr.Accordion("Expert Zone", open=False),inputs=None, outputs=[expert_zone]
201
- ).success(fn=finalize_letter_txt, inputs=[cheap_base, cheap_key, cheap_model, debug_CoT], outputs=[cover_letter_text]
202
- ).success(fn=finalize_letter_pdf, inputs=[cheap_base, cheap_key, cheap_model, jd_info, cv_text, cover_letter_text], outputs=[debug_jobapp, cover_letter_pdf])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
 
205
  if __name__ == "__main__":
 
9
 
10
  ## load data
11
  from _data_test import mock_jd, mock_cv
12
+
13
  ## ui
14
  import gradio as gr
15
+
16
  ## dependency
17
  from pypandoc.pandoc_download import download_pandoc
18
+
19
  ## std
20
  import os
21
  import json
22
 
23
+ logger = mylogger(__name__, "%(asctime)s:%(levelname)s:%(message)s")
24
  info = logger.info
25
 
26
+
27
  def init():
28
  os.system("shot-scraper install -b firefox")
29
  download_pandoc()
 
31
 
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 (
39
+ gr.Textbox(value=cheap_base, label="API Base", interactive=False),
40
+ gr.Textbox(
41
+ value=cheap_key, label="API key", type="password", interactive=False
42
+ ),
43
+ gr.Textbox(value=cheap_model, label="Model ID", interactive=False),
44
+ # setup_zone,
45
+ )
46
  else:
47
+ return (
48
+ gr.Textbox(value=cheap_base, label="API Base", interactive=True),
49
+ gr.Textbox(
50
+ value=cheap_key, label="API key", type="password", interactive=True
51
+ ),
52
+ gr.Textbox(value=cheap_model, label="Model ID", interactive=True),
53
+ # setup_zone,
54
+ )
55
+
56
 
57
  ## Main Functions
58
 
59
+
60
  def prepare_input(jd_info, cv_file: str, cv_text):
61
  if jd_info:
62
  if is_valid_url(jd_info):
 
78
  cv = mock_cv
79
  return jd, cv
80
 
81
+
82
  def run_refine(api_base, api_key, api_model, jd_info, cv_text):
83
+ jd, cv = jd_info, cv_text
84
  cheapAPI = {"base": api_base, "key": api_key, "model": api_model}
85
  taskAI = TaskAI(cheapAPI, temperature=0.2, max_tokens=2048) # max_tokens=2048
86
  info("API initialized")
 
91
  for result in gen:
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)
 
101
  result += response.delta
102
  yield result
103
 
104
+
105
  def finalize_letter_txt(api_base, api_key, api_model, debug_CoT):
106
  cheapAPI = {"base": api_base, "key": api_key, "model": api_model}
107
  taskAI = TaskAI(cheapAPI, temperature=0.2, max_tokens=2048)
108
  info("Finalizing letter ...")
109
+ result = ""
110
  for response in taskAI.purify_letter(full_text=debug_CoT):
111
  result += response.delta
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))
119
  pdf_context = json.loads(meta_data)
120
  pdf_context["letter_body"] = cover_letter_text
121
+ return meta_data, compile_pdf(
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,
 
137
 
138
  with gr.Row():
139
  with gr.Column(scale=1):
140
+ with gr.Accordion(
141
+ "AI setup (OpenAI-compatible LLM API)", open=False
142
+ ) as setup_zone:
143
+ is_debug = gr.Checkbox(label="Debug Mode", value=IS_DEBUG)
144
+
145
  gr.Markdown(
146
  "**Cheap AI**, an honest format converter and refiner, extracts essential info from job description and résumé, to reduce subsequent cost on Strong AI."
147
  )
148
  with gr.Group():
149
+ cheap_base = gr.Textbox(value=CHEAP_API_BASE, label="API Base")
150
+ cheap_key = gr.Textbox(
151
+ value=CHEAP_API_KEY, label="API key", type="password"
152
  )
 
153
  cheap_model = gr.Textbox(value=CHEAP_MODEL, label="Model ID")
154
  gr.Markdown(
155
  "---\n**Strong AI**, a thoughtful wordsmith, generates perfect cover letters to make both you and recruiters happy."
156
  )
157
+ is_same_cheap_strong = gr.Checkbox(
158
+ label="the same as Cheap AI", value=False, container=False
159
+ )
160
  with gr.Group():
161
+ strong_base = gr.Textbox(value=STRONG_API_BASE, label="API Base")
 
 
162
  strong_key = gr.Textbox(
163
  value=STRONG_API_KEY, label="API key", type="password"
164
  )
 
173
  )
174
  with gr.Group():
175
  gr.Markdown("## Applicant - CV / Résumé")
176
+ # with gr.Row():
177
  cv_file = gr.File(
178
  label="Allowed formats: " + " ".join(CV_EXT),
179
  file_count="single",
 
191
  min_jd = gr.TextArea(label="Reformatted Job Description")
192
  min_cv = gr.TextArea(label="Reformatted CV / Résumé")
193
  with gr.Accordion("Expert Zone", open=False) as expert_zone:
 
194
  debug_CoT = gr.Textbox(label="Chain of Thoughts")
195
  debug_jobapp = gr.Textbox(label="Job application meta data")
196
  cover_letter_text = gr.Textbox(label="Cover Letter")
 
201
  type="filepath",
202
  )
203
  infer_btn = gr.Button("Go!", variant="primary")
204
+
205
+ is_same_cheap_strong.change(
206
+ fn=set_same_cheap_strong,
207
+ inputs=[is_same_cheap_strong, cheap_base, cheap_key, cheap_model],
208
+ outputs=[strong_base, strong_key, strong_model],
209
+ )
210
+
211
+ infer_btn.click(
212
+ fn=set_same_cheap_strong,
213
+ inputs=[is_same_cheap_strong, cheap_base, cheap_key, cheap_model],
214
+ outputs=[strong_base, strong_key, strong_model],
215
  ).success(
216
+ fn=prepare_input, inputs=[jd_info, cv_file, cv_text], outputs=[jd_info, cv_text]
 
 
217
  ).success(
218
  fn=run_refine,
219
  inputs=[cheap_base, cheap_key, cheap_model, jd_info, cv_text],
220
  outputs=[min_jd, min_cv],
221
+ ).success(
222
+ fn=lambda: [
223
+ gr.Accordion("Expert Zone", open=True),
224
+ gr.Accordion("Reformatting", open=False),
225
+ ],
226
+ inputs=None,
227
+ outputs=[expert_zone, reformat_zone],
228
+ ).success(
229
+ fn=run_compose,
230
+ inputs=[strong_base, strong_key, strong_model, min_jd, min_cv],
231
+ outputs=[debug_CoT],
232
+ ).success(
233
+ fn=lambda: gr.Accordion("Expert Zone", open=False),
234
+ inputs=None,
235
+ outputs=[expert_zone],
236
+ ).success(
237
+ fn=finalize_letter_txt,
238
+ inputs=[cheap_base, cheap_key, cheap_model, debug_CoT],
239
+ outputs=[cover_letter_text],
240
+ ).success(
241
+ fn=finalize_letter_pdf,
242
+ inputs=[
243
+ cheap_base,
244
+ cheap_key,
245
+ cheap_model,
246
+ jd_info,
247
+ cv_text,
248
+ cover_letter_text,
249
+ ],
250
+ outputs=[debug_jobapp, cover_letter_pdf],
251
+ )
252
 
253
 
254
  if __name__ == "__main__":
taskAI.py CHANGED
@@ -83,7 +83,7 @@ class TaskAI(OpenAILike):
83
  return window_size
84
 
85
  checkAPI(api_base, api_key)
86
-
87
  super().__init__(
88
  api_base=api["base"],
89
  api_key=api["key"],
 
83
  return window_size
84
 
85
  checkAPI(api_base, api_key)
86
+
87
  super().__init__(
88
  api_base=api["base"],
89
  api_key=api["key"],
taskNonAI.py CHANGED
@@ -1,5 +1,6 @@
1
  import pypandoc
2
  import typst
 
3
  ## stdlib
4
  import subprocess
5
  import json
@@ -40,26 +41,37 @@ def extract_url(url: str) -> Optional[str]:
40
  f"Please try copy-paste as input. Failed to extract content from: {url}. Didn't find content from given URL!"
41
  )
42
 
43
- def _date()->str:
 
44
  current_date = datetime.now()
45
  return current_date.strftime(
46
- f"%B %d{'th' if 4 <= current_date.day <= 20 or 24 <= current_date.day <= 30 else ['st', 'nd', 'rd'][current_date.day % 10 - 1]} , %Y")
 
 
 
 
 
 
47
 
48
- def _typst_escape(s)->str:
49
- return str(s).replace('@','\@').replace('#','\#')
50
-
51
- def compile_pdf(context: dict, tmpl_path: str, output_path="/tmp/cover_letter.pdf", is_debug=False)->list[str]:
52
- letter_src_filepath = 'typst/letter.typ'
53
- with open(tmpl_path, "r", encoding='utf8') as f:
54
  tmpl = Template(f.read())
55
  context = {k: _typst_escape(v) for k, v in context.items()}
56
- context.update({'date_string': _date()})
57
  letter_typ = tmpl.safe_substitute(context)
58
- with open(letter_src_filepath, 'w', encoding='utf8') as f:
59
  f.write(letter_typ)
60
- typst.compile(letter_src_filepath, output_path, root=Path('./typst/'), font_paths=[Path('./fonts/')])
 
 
 
 
 
61
  # os.remove(letter_src_filepath)
62
  if is_debug:
63
  return [letter_src_filepath, output_path]
64
  else:
65
- return [output_path]
 
1
  import pypandoc
2
  import typst
3
+
4
  ## stdlib
5
  import subprocess
6
  import json
 
41
  f"Please try copy-paste as input. Failed to extract content from: {url}. Didn't find content from given URL!"
42
  )
43
 
44
+
45
+ def _date() -> str:
46
  current_date = datetime.now()
47
  return current_date.strftime(
48
+ f"%B %d{'th' if 4 <= current_date.day <= 20 or 24 <= current_date.day <= 30 else ['st', 'nd', 'rd'][current_date.day % 10 - 1]} , %Y"
49
+ )
50
+
51
+
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
58
+ ) -> list[str]:
59
+ letter_src_filepath = "typst/letter.typ"
60
+ with open(tmpl_path, "r", encoding="utf8") as f:
 
61
  tmpl = Template(f.read())
62
  context = {k: _typst_escape(v) for k, v in context.items()}
63
+ context.update({"date_string": _date()})
64
  letter_typ = tmpl.safe_substitute(context)
65
+ with open(letter_src_filepath, "w", encoding="utf8") as f:
66
  f.write(letter_typ)
67
+ typst.compile(
68
+ letter_src_filepath,
69
+ output_path,
70
+ root=Path("./typst/"),
71
+ font_paths=[Path("./fonts/")],
72
+ )
73
  # os.remove(letter_src_filepath)
74
  if is_debug:
75
  return [letter_src_filepath, output_path]
76
  else:
77
+ return [output_path]
util.py CHANGED
@@ -22,7 +22,7 @@ def mylogger(name, format, level=logging.INFO):
22
  return logger
23
 
24
 
25
- def count_token(text, encoding="cl100k_base")->int:
26
  return len(tiktoken.get_encoding(encoding).encode(text))
27
 
28
 
@@ -41,9 +41,13 @@ def is_valid_openai_api_key(api_base: str, api_key: str) -> bool:
41
 
42
  return response.status_code == 200
43
 
 
44
  def checkAPI(api_base: str, api_key: str):
45
  if not is_valid_openai_api_key(api_base, api_key):
46
- raise ValueError("Invalid API key or less possibly OpenAI's (or AI provider's) fault. Did you setup your AI APIs properly? If you don't have any API key, try get one from https://beta.openai.com/account/api-keys")
 
 
 
47
 
48
  def zip_api(api_base: str, api_key: str, model: str) -> dict[str, str]:
49
  return {"base": api_base, "key": api_key, "model": model}
 
22
  return logger
23
 
24
 
25
+ def count_token(text, encoding="cl100k_base") -> int:
26
  return len(tiktoken.get_encoding(encoding).encode(text))
27
 
28
 
 
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
+ "Invalid API key or less possibly OpenAI's (or AI provider's) fault. Did you setup your AI APIs properly? If you don't have any API key, try get one from https://beta.openai.com/account/api-keys"
49
+ )
50
+
51
 
52
  def zip_api(api_base: str, api_key: str, model: str) -> dict[str, str]:
53
  return {"base": api_base, "key": api_key, "model": model}