feat: add subscribtion scheduler with ai, pagination
This commit is contained in:
112
ai.py
Normal file
112
ai.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import aiohttp
|
||||
import json
|
||||
import re
|
||||
from config import OPENROUTER
|
||||
|
||||
SYSTEM_PROMPT_TEMPLATE = """
|
||||
Ты — экспертный технический рекрутер. Твоя задача: отфильтровать IT-вакансии.
|
||||
Предпочтения пользователя:
|
||||
{preferences}
|
||||
|
||||
Верни ТОЛЬКО валидный JSON массив объектов, которые подходят под стек и грейд.
|
||||
Если ничего не подходит, верни [].
|
||||
Никаких пояснений и markdown-разметки.
|
||||
"""
|
||||
|
||||
def get_forced_vacancies(vacancies: list, user_prefs: str) -> list:
|
||||
stack_match = re.search(r'(?i)стек:(.*?)(?=доп:|$)', user_prefs, re.DOTALL)
|
||||
if not stack_match:
|
||||
return []
|
||||
|
||||
stack_content = stack_match.group(1).lower()
|
||||
|
||||
all_keywords = re.findall(r'[a-zA-Z0-9.+#]{2,}', stack_content)
|
||||
|
||||
common_words = {'html', 'css', 'git', 'sql', 'api', 'rest', 'remote', 'work'}
|
||||
strong_keys = list(set(word for word in all_keywords if word not in common_words))
|
||||
|
||||
if not strong_keys:
|
||||
return []
|
||||
|
||||
print(f"🔍 Ключи для авто-добавления: {strong_keys}")
|
||||
|
||||
forced = []
|
||||
for vac in vacancies:
|
||||
v_id = str(vac.get('id', ''))
|
||||
vac_str = json.dumps(vac, ensure_ascii=False).lower()
|
||||
vac_clean = re.sub(r'https?://\S+', '', vac_str)
|
||||
|
||||
if any(key in vac_clean for key in strong_keys):
|
||||
forced.append(vac)
|
||||
|
||||
return forced
|
||||
|
||||
async def filter_vacancies_with_ai(vacancies: list, user_prefs: str) -> list:
|
||||
if not vacancies:
|
||||
return []
|
||||
|
||||
forced_vacancies = get_forced_vacancies(vacancies, user_prefs)
|
||||
forced_ids = {str(v.get('id')) for v in forced_vacancies if v.get('id')}
|
||||
|
||||
remaining_vacancies = [v for v in vacancies if str(v.get('id')) not in forced_ids]
|
||||
|
||||
if not remaining_vacancies:
|
||||
print(f"✅ Все {len(forced_vacancies)} вакансий одобрены автоматом.")
|
||||
return forced_vacancies
|
||||
|
||||
print(f"✅ Авто-одобрено: {len(forced_vacancies)}")
|
||||
print(f"🤖 Отправка на AI: {len(remaining_vacancies)} (было {len(vacancies)})")
|
||||
|
||||
api_url = "https://openrouter.ai/api/v1/chat/completions"
|
||||
|
||||
system_prompt = SYSTEM_PROMPT_TEMPLATE.format(preferences=user_prefs)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
clean_token = str(OPENROUTER).strip()
|
||||
|
||||
async with session.post(
|
||||
url=api_url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {clean_token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"model": "openrouter/free",
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": json.dumps(remaining_vacancies, ensure_ascii=False)}
|
||||
]
|
||||
},
|
||||
timeout=60
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
raw_err = await response.text()
|
||||
print(f"❌ Ошибка API ({response.status}): {raw_err[:100]}")
|
||||
return forced_vacancies
|
||||
|
||||
result_raw = await response.json()
|
||||
|
||||
if 'choices' not in result_raw:
|
||||
return forced_vacancies
|
||||
|
||||
content = result_raw['choices'][0]['message']['content'].strip()
|
||||
|
||||
match = re.search(r'\[\s*\{.*\}\s*\]', content, re.DOTALL)
|
||||
clean_json = match.group(0) if match else content.replace('```json', '').replace('```', '').strip()
|
||||
|
||||
try:
|
||||
ai_vacs = json.loads(clean_json)
|
||||
final = forced_vacancies.copy()
|
||||
for v in ai_vacs:
|
||||
if str(v.get('id')) not in forced_ids:
|
||||
final.append(v)
|
||||
|
||||
print(f"🏁 Итог: {len(final)} релевантных вакансий")
|
||||
return final
|
||||
except:
|
||||
return forced_vacancies
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка в блоке запроса: {e}")
|
||||
return forced_vacancies
|
||||
Reference in New Issue
Block a user