curated jobs and tags

This commit is contained in:
Doar Mihai
2026-04-04 20:37:35 +03:00
parent feb964f3f6
commit adca106244
10 changed files with 282 additions and 4 deletions
+122
View File
@@ -0,0 +1,122 @@
<?php
namespace App\Console\Commands;
use App\Models\Job;
use App\Models\Tag;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
#[Signature('jobs:curate {--batch=50}')]
#[Description('Curate jobs by fetching tags from an external API')]
class CurateJobs extends Command
{
/**
* Execute the console command.
*/
public function handle(): int
{
$batchSize = (int) $this->option('batch');
//$apiUrl = $this->option('api-url');
$apiUrl = 'https://loki.c0rrup7.net';
if (empty($apiUrl)) {
$this->error('API URL is required. Use --api-url option to provide the endpoint.');
return self::FAILURE;
}
$this->info('Starting job curation...');
$this->newLine();
// Fetch uncurated jobs
$uncuratedJobs = Job::uncurated()
->limit($batchSize)
->get();
if ($uncuratedJobs->isEmpty()) {
$this->info('No uncurated jobs found.');
return self::SUCCESS;
}
$this->info("Found {$uncuratedJobs->count()} uncurated jobs.");
$this->newLine();
$curatedCount = 0;
$failedCount = 0;
$progressBar = $this->output->createProgressBar($uncuratedJobs->count());
$progressBar->start();
foreach ($uncuratedJobs as $job) {
try {
$body = 'This is the content of a job ad, you need to read it and extract the relevant tags from it. Respond only with the tags in JSON format, NO OTHER TEXT BEFOR OR AFTER THE JSON.';
$body .= 'Title: '. htmlentities($job->title);
$body .= 'Description: '. htmlentities($job->description);
// Make POST request to API with job name and description
$response = Http::timeout(130)
->withHeaders([
'Accept' => 'application/json',
'X-Api-Key' => '1620539ff09a897a7281a8b626c145e5',
])
->post($apiUrl, [
'service' => 'ollama',
'model' => 'llama3',
'stream' => false,
'prompt' => $body,
]);
if ($response->successful()) {
$tags = $response->json();
if (is_array($tags) && !empty($tags)) {
$tagIds = [];
foreach ($tags as $tagName) {
// Find or create tag
$tag = Tag::firstOrCreate(
['name' => $tagName]
);
$tagIds[] = $tag->id;
}
// Sync tags with the job
$job->tags()->sync($tagIds);
// Mark job as curated
$job->update(['is_curated' => true]);
$curatedCount++;
} else {
// Mark as curated even if no tags returned
$job->update(['is_curated' => true]);
$curatedCount++;
}
} else {
$failedCount++;
Log::warning("Failed to curate job #{$job->id}: API returned {$response->status()}");
}
} catch (\Exception $e) {
$failedCount++;
Log::error("Error curating job #{$job->id}: {$e->getMessage()}");
}
$progressBar->advance();
}
$progressBar->finish();
$this->newLine(2);
$this->info("Successfully curated {$curatedCount} jobs.");
if ($failedCount > 0) {
$this->warn("Failed to curate {$failedCount} jobs. Check logs for details.");
}
return self::SUCCESS;
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ class JobController extends Controller
public function show(Job $job)
{
// Load relationships
$job->load(['source', 'category', 'jobLocation']);
$job->load(['source', 'category', 'jobLocation', 'tags']);
// Track job view event
AnalyticsEvent::logEvent(AnalyticsEvent::EVENT_JOB_VIEW, $job->id);
+18
View File
@@ -46,6 +46,7 @@ class Job extends Model
'posted_at',
'expires_at',
'remote_allowed',
'is_curated',
'experience_level',
'job_category_id',
'job_location_id',
@@ -56,6 +57,7 @@ class Job extends Model
'posted_at' => 'datetime',
'expires_at' => 'datetime',
'remote_allowed' => 'boolean',
'is_curated' => 'boolean',
'salary_min' => 'decimal:2',
'salary_max' => 'decimal:2',
];
@@ -93,6 +95,14 @@ class Job extends Model
->withTimestamps();
}
/**
* Get the tags for this job.
*/
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
/**
* Scope a query to only include active jobs.
*/
@@ -104,6 +114,14 @@ class Job extends Model
});
}
/**
* Scope a query to only include uncurated jobs.
*/
public function scopeUncurated($query)
{
return $query->where('is_curated', false);
}
/**
* Scope a query to only include remote jobs.
*/
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Str;
class Tag extends Model
{
protected $fillable = ['name', 'slug'];
protected static function boot()
{
parent::boot();
static::creating(function ($tag) {
if (empty($tag->slug)) {
$tag->slug = Str::slug($tag->name);
}
});
}
public function jobs(): BelongsToMany
{
return $this->belongsToMany(Job::class);
}
}
@@ -80,9 +80,11 @@ class AdzunaTransformer implements JobTransformer
$description = strtolower($data['description'] ?? '');
$location = strtolower($data['location']['display_name'] ?? '');
return str_contains($title, 'remote') ||
str_contains($description, 'remote') ||
str_contains($location, 'remote');
// this gives false positives, find a better way, until then should be false
// return str_contains($title, 'remote') ||
// str_contains($description, 'remote') ||
// str_contains($location, 'remote');
return false;
}
private function parseDate(?string $date): ?DateTime