Skip to content

Commit aa07d1e

Browse files
authored
Merge pull request #11 from HardMax71/dev
Dev
2 parents f6d47b8 + 1c7c69f commit aa07d1e

31 files changed

+670
-533
lines changed

backend/backend/urls.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
from typing import Any
2-
31
from django.contrib import admin
4-
from django.http import HttpResponse
2+
from django.http import HttpRequest, HttpResponse
53
from django.urls import include, path
64
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
75
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
@@ -10,7 +8,7 @@
108
from processor.services.job_service import JobService
119

1210

13-
async def metrics_view(_request: Any) -> HttpResponse:
11+
async def metrics_view(_request: HttpRequest) -> HttpResponse:
1412
try:
1513
job_service = JobService()
1614
stats = await job_service.get_queue_stats()

backend/core/domain/resume.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,6 @@ class Contact(BaseModel):
6363
email: str
6464
phone: str | None = None
6565
links: ContactLinks | None = None
66-
# Direct access for legacy compatibility
67-
linkedin: str | None = None
68-
github: str | None = None
69-
website: str | None = None
7066

7167

7268
class WorkAuthorization(BaseModel):
@@ -311,9 +307,6 @@ class LanguageProficiency(BaseModel):
311307
language: Language
312308
self_assessed: str
313309
cefr: str
314-
# Legacy/alternative field names
315-
name: str | None = None
316-
level: str | None = None
317310

318311
@model_validator(mode="before")
319312
@classmethod
@@ -331,10 +324,6 @@ class Award(BaseModel):
331324
position: str | None = None
332325
description: str | None = None
333326
url: str | None = None
334-
# Legacy/alternative field names
335-
title: str | None = None
336-
issuer: str | None = None
337-
date: str | None = None
338327

339328

340329
class ScientificContribution(BaseModel):
@@ -372,15 +361,6 @@ class Resume(BaseModel):
372361
language_proficiency: list[LanguageProficiency] = Field(default_factory=list)
373362
awards: list[Award] = Field(default_factory=list)
374363
scientific_contributions: list[ScientificContribution] = Field(default_factory=list)
375-
# Legacy flat fields for backward compatibility
376-
name: str | None = None
377-
email: str | None = None
378-
phone: str | None = None
379-
location: Location | None = None
380-
linkedin: str | None = None
381-
github: str | None = None
382-
website: str | None = None
383-
resume_lang: str | None = None
384364

385365
@model_validator(mode="before")
386366
@classmethod

backend/processor/views.py

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import logging
2-
from typing import Any
32

43
from adrf.views import APIView
54
from django.conf import settings
65
from django.core.cache import cache as django_cache
76
from django.utils.decorators import method_decorator
87
from django.views.decorators.cache import cache_page
8+
from drf_spectacular.extensions import OpenApiSerializerExtension
99
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
10-
from pydantic import BaseModel, Field
11-
from rest_framework import status
10+
from rest_framework import serializers, status
1211
from rest_framework.exceptions import NotFound
1312
from rest_framework.pagination import LimitOffsetPagination
1413
from rest_framework.request import Request
@@ -24,13 +23,65 @@
2423
THROTTLE_RATES: dict[str, str] = settings.REST_FRAMEWORK.get("DEFAULT_THROTTLE_RATES", {}) # type: ignore[assignment]
2524

2625

27-
class ResumeListResponse(BaseModel):
28-
"""Paginated list of resumes."""
26+
class QueueMetricsSerializer(serializers.Serializer):
27+
stream_length = serializers.IntegerField(help_text="Total items in Redis stream")
28+
queue_length = serializers.IntegerField(help_text="Pending items in queue")
29+
scheduled_retries = serializers.IntegerField(help_text="Items scheduled for retry")
30+
active_jobs = serializers.IntegerField(help_text="Currently processing jobs")
31+
redis_memory_usage = serializers.IntegerField(help_text="Redis memory usage in bytes")
2932

30-
count: int = Field(description="Total number of resumes")
31-
next: str | None = Field(default=None, description="Next page URL")
32-
previous: str | None = Field(default=None, description="Previous page URL")
33-
results: list[ResumeResponse] = Field(description="Resumes for current page")
33+
34+
class ProcessingConfigSerializer(serializers.Serializer):
35+
text_llm_provider = serializers.CharField(help_text="LLM provider for text extraction")
36+
text_llm_model = serializers.CharField(help_text="LLM model for text extraction")
37+
ocr_llm_provider = serializers.CharField(help_text="LLM provider for OCR")
38+
ocr_llm_model = serializers.CharField(help_text="LLM model for OCR")
39+
generate_review = serializers.BooleanField(help_text="Whether AI review is enabled")
40+
store_in_db = serializers.BooleanField(help_text="Whether to store in database")
41+
42+
43+
class HealthResponseSerializer(serializers.Serializer):
44+
status = serializers.CharField(help_text="Service status: ok, degraded, or down")
45+
service = serializers.CharField(help_text="Service name")
46+
queue = QueueMetricsSerializer()
47+
processing_config = ProcessingConfigSerializer()
48+
49+
50+
class FileTypeConfigSerializer(serializers.Serializer):
51+
"""Configuration for a single file type."""
52+
53+
media_type = serializers.CharField(help_text="MIME type")
54+
category = serializers.CharField(help_text="File category (document, image, etc)")
55+
max_size_mb = serializers.IntegerField(help_text="Maximum file size in MB")
56+
parser = serializers.CharField(help_text="Parser used for this file type")
57+
58+
59+
class FileConfigResponseSerializer(serializers.Serializer):
60+
"""Dictionary of file type configurations keyed by extension."""
61+
62+
def to_representation(self, instance: dict) -> dict:
63+
"""Return the dict as-is since keys are dynamic extensions."""
64+
return instance
65+
66+
67+
class FileConfigResponseExtension(OpenApiSerializerExtension):
68+
"""Extension to generate proper additionalProperties schema for FileConfigResponse."""
69+
70+
target_class = "processor.views.FileConfigResponseSerializer"
71+
72+
def map_serializer(self, auto_schema, direction):
73+
"""Generate object schema with additionalProperties pointing to FileTypeConfig."""
74+
return {
75+
"type": "object",
76+
"additionalProperties": auto_schema.resolve_serializer(FileTypeConfigSerializer, direction).ref,
77+
}
78+
79+
80+
class ResumeListResponse(serializers.Serializer):
81+
count = serializers.IntegerField(help_text="Total number of resumes")
82+
next = serializers.CharField(allow_null=True, help_text="Next page URL")
83+
previous = serializers.CharField(allow_null=True, help_text="Previous page URL")
84+
results = serializers.ListField(help_text="Resumes for current page")
3485

3586

3687
class ResumeCollectionView(APIView):
@@ -135,7 +186,7 @@ class HealthView(APIView):
135186
"""Service health status endpoint."""
136187

137188
@extend_schema(
138-
responses={200: OpenApiResponse(description="Health status including queue stats and configuration")},
189+
responses={200: HealthResponseSerializer},
139190
description="Get service health status. Returns system status, queue statistics, and processing configuration.",
140191
)
141192
async def get(self, request: Request) -> Response:
@@ -159,11 +210,11 @@ class FileConfigView(APIView):
159210
"""File upload configuration endpoint."""
160211

161212
@extend_schema(
162-
responses={200: OpenApiResponse(description="File upload configuration")},
163-
description="Get file upload configuration. Returns allowed extensions, MIME types, max sizes, and categories.",
213+
responses={200: FileConfigResponseSerializer},
214+
description="Get file upload configuration. Returns allowed extensions, MIME types, max sizes, and categories. Keys are file extensions.",
164215
)
165216
@method_decorator(cache_page(60 * 60 * 24))
166-
def get(self, request: Any) -> Response:
217+
def get(self, request: Request) -> Response:
167218
cache_key = "file_config_v1"
168219
cached = django_cache.get(cache_key)
169220
if cached:

backend/rag/views.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rest_framework.request import Request
77
from rest_framework.response import Response
88

9+
from core.domain.rag import CandidateComparison, InterviewQuestionSet, JobMatchExplanation
910
from core.metrics import RAG_GENERATION_COUNT, RAG_GENERATION_DURATION
1011

1112
from .serializers import (
@@ -23,7 +24,7 @@ class ExplainMatchView(APIView):
2324
@extend_schema(
2425
request=ExplainMatchRequestSerializer,
2526
responses={
26-
200: OpenApiResponse(description="Structured match explanation"),
27+
200: JobMatchExplanation,
2728
404: OpenApiResponse(description="Resume not found"),
2829
},
2930
description="Generate AI-powered explanation of candidate-job fit with structured strengths, concerns, and recommendations.",
@@ -53,7 +54,7 @@ class CompareCandidatesView(APIView):
5354
@extend_schema(
5455
request=CompareCandidatesRequestSerializer,
5556
responses={
56-
200: OpenApiResponse(description="Structured candidate comparison"),
57+
200: CandidateComparison,
5758
400: OpenApiResponse(description="Invalid request"),
5859
404: OpenApiResponse(description="One or more resumes not found"),
5960
},
@@ -85,7 +86,7 @@ class InterviewQuestionsView(APIView):
8586
@extend_schema(
8687
request=InterviewQuestionsRequestSerializer,
8788
responses={
88-
200: OpenApiResponse(description="Structured interview question set"),
89+
200: InterviewQuestionSet,
8990
404: OpenApiResponse(description="Resume not found"),
9091
},
9192
description="Generate 6-12 interview questions tailored to candidate's background, with follow-ups and assessment criteria.",

backend/search/serializers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ class SearchResponseSerializer(serializers.Serializer):
234234
results = SearchResultSerializer(many=True, help_text="Search results")
235235
query = serializers.CharField(required=False, allow_null=True, allow_blank=True, help_text="Original query")
236236
search_type = serializers.CharField(help_text="Type of search performed")
237+
total_found = serializers.IntegerField(help_text="Total number of matching results found")
237238

238239

239240
class FilterOptionSerializer(serializers.Serializer):
@@ -255,7 +256,10 @@ class CountryOptionSerializer(serializers.Serializer):
255256

256257
class EducationLevelOptionSerializer(serializers.Serializer):
257258
level = serializers.CharField(help_text="Education level")
258-
statuses = serializers.ListField(child=serializers.CharField(), help_text="Education statuses for this level")
259+
statuses = serializers.ListField(
260+
child=serializers.ChoiceField(choices=[status.value for status in EducationStatus]),
261+
help_text="Education statuses for this level",
262+
)
259263
resume_count = serializers.IntegerField(help_text="Number of resumes with this level")
260264

261265

frontend/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Route, Routes, useLocation } from "react-router-dom";
1+
import { Route, Routes } from "react-router-dom";
22
import Header from "./components/Header";
33
import Footer from "./components/Footer";
44
import PersistentSelectionBar from "./components/PersistentSelectionBar";

frontend/src/api/generated/sdk.gen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { V1ConfigFileTypesRetrieveError, V1ConfigFileTypesRetrieveResponse,
66
export const client = createClient(createConfig());
77

88
/**
9-
* Get file upload configuration. Returns allowed extensions, MIME types, max sizes, and categories.
9+
* Get file upload configuration. Returns allowed extensions, MIME types, max sizes, and categories. Keys are file extensions.
1010
*/
1111
export const v1ConfigFileTypesRetrieve = <ThrowOnError extends boolean = false>(options?: OptionsLegacyParser<unknown, ThrowOnError>) => {
1212
return (options?.client ?? client).get<V1ConfigFileTypesRetrieveResponse, V1ConfigFileTypesRetrieveError, ThrowOnError>({

0 commit comments

Comments
 (0)