Skip to content

Commit bdde56f

Browse files
committed
Merge with work
2 parents 279d9bc + 82dfe1e commit bdde56f

22 files changed

+1391
-436
lines changed

LICENSE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
77
The above copyright notice and this permission notice must be included in all copies or substantial portions of the Software.
88

99
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10+
11+
Additional Notices
12+
13+
This project is an active fork of Jobs_Applier_AI_Agent_AIHawk (https://github.com/feder-cr/Jobs_Applier_AI_Agent_AIHawk), which is MIT-licensed. Portions of this code are derived from that project. Original authors and contributors retain their copyrights in those portions.

README.md

Lines changed: 114 additions & 49 deletions
Large diffs are not rendered by default.

TODO

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
- check agent pipline with headless mode
2-
3-
- Add tests for:
4-
- job_manager (those methods that don't interact with the browser)
5-
61
Finsihing moves:
72
- Remove address of your tg chat from app_config.py
8-
- Create new repo for this project
9-
- Remove playwright examples
10-
- Take the video of its work
113
- Write an article for Medium, Reddit, Dev.to, Hashnode, Opensource.com

config/app_config.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
otherwise ask LLM to select only those vacancies that suit you
1414
by interests or by tech stack
1515
"""
16-
MONKEY_MODE = True
16+
MONKEY_MODE = False
1717

1818
"""
1919
In this mode app doesn't apply to the jobs, only creates resumes, cover letters and gathers skill statistics
@@ -23,7 +23,6 @@
2323
"""
2424
TEST_MODE = False
2525

26-
2726
"""
2827
In this mode app doesn't apply to the jobs or create resumes and cover letters, only gathers information for interesting jobs and
2928
their skill statistics and saves them to the files data/output/interesting_jobs.yaml and data/output/skill_stat.yaml."""
@@ -34,13 +33,13 @@
3433
If this mode is deactivated, app will apply to the jobs with Easy Apply and try to apply to the jobs with 3rd party applications
3534
WARNING: applying to the jobs with 3rd party applications is not guaranteed to be successful, but is guaranteed to consume at least 10-100x more tokens
3635
"""
37-
EASY_APPLY_ONLY_MODE = False
36+
EASY_APPLY_ONLY_MODE = True
3837

3938
"""
40-
If this mode is activated, app will check if the last search was less than a day ago.
39+
If this mode is activated, app will check if the last search was less than a day ago.
4140
This is useful if you want bot to automatically restart the search every 24 hours when LinkedIn resets the search limits.
4241
"""
43-
CHECK_LAST_SEARCH_TIME = False
42+
RESTART_EVERY_DAY = False
4443

4544
"""
4645
If LLM evaluated the 'interest' level of the job not below this threshold - the job is considered interesting for application.
@@ -51,6 +50,16 @@
5150
"""Minimum time spent on one job application"""
5251
MINIMUM_WAIT_TIME_SEC = 10
5352

53+
"""
54+
If this mode is activated, app will try to decrease RPM to avoid rate limit errors
55+
"""
56+
FREE_TIER = False
57+
58+
"""
59+
Free tier mode wait time in seconds
60+
"""
61+
FREE_TIER_RPM_LIMIT = 15
62+
5463
"""Telegram chat address and corresponding topic IDs for sending"""
5564
TG_CHAT_ID = "@linkedin_feedback"
5665
TG_ERR_TOPIC_ID = 2
@@ -86,7 +95,7 @@
8695
APPLY_AGENT_MODEL = "gemini-flash-latest"
8796

8897
"""
89-
Model temperature
98+
Easy Apply model temperature
9099
the higher it is, the more creative the model, but hallucinations may occur
91100
the lower it is, the more strictly the model follows the prompt and invents less
92101
"""

config/logger_config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from config.constants import LOG_DIR
88
from src.telegram.telegram_error_handler import AsyncTelegramSink
99

10-
1110
logger.remove()
1211

1312
if MINIMUM_LOG_LEVEL in ["DEBUG", "TRACE", "INFO", "WARNING", "ERROR", "CRITICAL"]:

examples/.env_example

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,4 @@ linkedin_password="[Your LinkedIn Password]"
44
llm_api_key="[Your LLM API Key]"
55
llm_proxy="[Your LLM Proxy in format http(s)://login:password@host:port]"
66

7-
tg_api_id="[Your Telegram API ID]"
8-
tg_api_hash="[Your Telegram API Hash]"
97
tg_token="[Your Telegram Bot Token]"

main.py

Lines changed: 97 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import time
66
import traceback
77
from pathlib import Path
8+
from threading import Lock
89

910
import dotenv
11+
from pynput import keyboard as pynput_kb
1012

11-
from config.app_config import CHECK_LAST_SEARCH_TIME
13+
from config.app_config import RESTART_EVERY_DAY
1214
from config.constants import BROWSER_STORAGE_STATE, RESUME_DIR, SEARCH_CONFIG_FILE
1315
from config.logger_config import logger
1416
from src.job_manager.authenticator import LinkedInAuthenticator
@@ -36,6 +38,11 @@
3638
RESUME_TEXT_FILE = Path(RESUME_DIR) / "resume_text.txt"
3739
READY_MADE_RESUME = Path(RESUME_DIR) / "resume.pdf"
3840

41+
# Global pause state for keyboard control
42+
paused = False
43+
pause_lock = Lock()
44+
ctrl_pressed = False
45+
3946

4047
class ConfigError(Exception):
4148
pass
@@ -101,6 +108,49 @@ def validate_resume_structured(resume_structured_file: Path) -> dict:
101108
raise ConfigError(f"Structured resume validation error: {str(e)}")
102109

103110

111+
def on_press(key):
112+
"""Handle key press events"""
113+
global paused, ctrl_pressed
114+
try:
115+
# Track Ctrl key state
116+
if key in (pynput_kb.Key.ctrl_l, pynput_kb.Key.ctrl_r):
117+
ctrl_pressed = True
118+
# Check for 'x' key when Ctrl is pressed
119+
elif hasattr(key, "char") and key.char == "x" and ctrl_pressed:
120+
with pause_lock:
121+
paused = not paused
122+
if paused:
123+
logger.warning("⏸️ PAUSED - Press Ctrl+X to continue")
124+
else:
125+
logger.info("▶️ RESUMED")
126+
except AttributeError:
127+
pass
128+
129+
130+
def on_release(key):
131+
"""Handle key release events"""
132+
global ctrl_pressed
133+
# Reset Ctrl key state
134+
if key in (pynput_kb.Key.ctrl_l, pynput_kb.Key.ctrl_r):
135+
ctrl_pressed = False
136+
137+
138+
def start_keyboard_listener():
139+
"""Start keyboard listener in background thread"""
140+
listener = pynput_kb.Listener(on_press=on_press, on_release=on_release)
141+
listener.daemon = True
142+
listener.start()
143+
logger.info("Keyboard listener started - Press Ctrl+X to pause/resume")
144+
145+
146+
async def check_pause():
147+
"""Check if execution is paused and wait if needed"""
148+
global paused
149+
if paused:
150+
while paused:
151+
await asyncio.sleep(0.5)
152+
153+
104154
async def create_and_run_bot(
105155
search_config: dict,
106156
secrets: dict,
@@ -170,9 +220,10 @@ async def create_and_run_bot(
170220
# Set bot facade
171221
bot = BotFacade(resume_anonymizer, search_component, apply_component, llm_agent_component)
172222
bot.set_parameters(search_config)
223+
bot.set_pause_checker(check_pause)
173224

174225
# Check if the last search was less than a day ago
175-
if CHECK_LAST_SEARCH_TIME and not apply_component.check_the_last_search_time():
226+
if RESTART_EVERY_DAY and not apply_component.check_the_last_search_time():
176227
logger.warning(
177228
"Last search was less than a day ago, finishing work. If you want to restart the search, delete the file data/output/last_run.yaml file"
178229
)
@@ -206,44 +257,50 @@ async def create_and_run_bot(
206257

207258

208259
def main() -> None:
209-
try:
210-
# create output folder if it doesn't exist
211-
data = Path("data")
212-
output_folder = data / "output"
213-
output_folder.mkdir(exist_ok=True)
214-
215-
# validate config files
216-
config_validator = ConfigValidator()
217-
secrets = config_validator.validate_secrets()
218-
search_config = config_validator.validate_search_config(SEARCH_CONFIG_FILE)
219-
resume_text = config_validator.validate_resume_text(RESUME_TEXT_FILE)
220-
resume_structured = config_validator.validate_resume_structured(RESUME_STRUCTURED_FILE)
221-
222-
logger.info("Starting LinkedIn Job Applier...")
223-
logger.info(f"Search config loaded with {len(search_config)} parameters")
224-
225-
# Run LinkedIn bot (async)
226-
asyncio.run(create_and_run_bot(search_config, secrets, resume_text, resume_structured))
227-
logger.info("LinkedIn bot completed successfully")
228-
229-
# Wait 1 hour total before next run
230-
if CHECK_LAST_SEARCH_TIME:
231-
time.sleep(3600)
232-
233-
except ConfigError as ce:
234-
logger.error(f"Configuration error: {str(ce)}")
235-
except FileNotFoundError as fnf:
236-
tb_str = traceback.format_exc()
237-
logger.error(f"File not found: {str(fnf)}\n{tb_str}")
238-
except RuntimeError as re:
239-
tb_str = traceback.format_exc()
240-
logger.error(f"Runtime error: {str(re)}\n{tb_str}")
241-
except Exception as e:
242-
tb_str = traceback.format_exc()
243-
logger.error(f"Unknown error: {str(e)}\n{tb_str}")
244-
finally:
245-
logger.info("Program completed")
246-
# time.sleep(600) # Commented out for testing
260+
# Start keyboard listener for pause/resume functionality
261+
start_keyboard_listener()
262+
263+
while True:
264+
try:
265+
# create output folder if it doesn't exist
266+
data = Path("data")
267+
output_folder = data / "output"
268+
output_folder.mkdir(exist_ok=True)
269+
270+
# validate config files
271+
config_validator = ConfigValidator()
272+
secrets = config_validator.validate_secrets()
273+
search_config = config_validator.validate_search_config(SEARCH_CONFIG_FILE)
274+
resume_text = config_validator.validate_resume_text(RESUME_TEXT_FILE)
275+
resume_structured = config_validator.validate_resume_structured(RESUME_STRUCTURED_FILE)
276+
277+
logger.info("Starting LinkedIn Job Applier...")
278+
logger.info(f"Search config loaded with {len(search_config)} parameters")
279+
280+
# Run LinkedIn bot (async)
281+
asyncio.run(create_and_run_bot(search_config, secrets, resume_text, resume_structured))
282+
logger.info("LinkedIn bot completed successfully")
283+
284+
except ConfigError as ce:
285+
logger.error(f"Configuration error: {str(ce)}")
286+
except FileNotFoundError as fnf:
287+
tb_str = traceback.format_exc()
288+
logger.error(f"File not found: {str(fnf)}\n{tb_str}")
289+
except RuntimeError as re:
290+
tb_str = traceback.format_exc()
291+
logger.error(f"Runtime error: {str(re)}\n{tb_str}")
292+
except Exception as e:
293+
tb_str = traceback.format_exc()
294+
logger.error(f"Unknown error: {str(e)}\n{tb_str}")
295+
finally:
296+
logger.info("Program completed")
297+
# Wait 1 hour total before next run
298+
if RESTART_EVERY_DAY:
299+
logger.info("Waiting 1 hour before next run")
300+
time.sleep(3600)
301+
else:
302+
logger.info("Exiting program")
303+
break
247304

248305

249306
if __name__ == "__main__":

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies = [
2727
"PyYAML==6.0.2",
2828
"reportlab==4.2.2",
2929
"telethon==1.39.0",
30+
"pynput==1.8.1",
3031
"python-telegram-bot==21.9",
3132
]
3233

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ Levenshtein==0.25.1
1414
loguru==0.7.2
1515
playwright
1616
pydantic>=2.0.02
17+
pynput==1.7.7
1718
PyYAML==6.0.2
1819
reportlab==4.2.2
1920
telethon==1.39.0
21+
pynput==1.8.1
2022
python-telegram-bot==21.9

src/job_manager/bot_facade.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ def set_resume_generator(self, resume_generator_manager) -> None:
101101
self.apply_component.set_resume_generator_manager(resume_generator_manager)
102102
logger.info("Resume manager successfully started")
103103

104+
def set_pause_checker(self, pause_checker) -> None:
105+
"""Set pause checker function for pausing execution"""
106+
logger.info("Setting pause checker function")
107+
self.apply_component.set_pause_checker(pause_checker)
108+
logger.info("Pause checker successfully set")
109+
104110
async def start_apply(self) -> None:
105111
"""Start resume sending process (async)"""
106112
self.state.validate_state(

0 commit comments

Comments
 (0)