113 lines
4.3 KiB
Python
113 lines
4.3 KiB
Python
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
|