Spaces:
Sleeping
Sleeping
Commit
·
f2d72fd
1
Parent(s):
6809354
Add application file
Browse files- Dockerfile +32 -0
- README.md +23 -1
- app.py +61 -0
- data/jobs.json +30 -0
- llm_client.py +65 -0
- models_schemas.py +30 -0
- parsers.py +29 -0
- prompts.py +51 -0
- requirements.txt +10 -0
- tests/sample.http +0 -0
Dockerfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Leia: https://huggingface.co/docs/hub/spaces-sdks-docker
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Usuário não-root (boa prática em Spaces)
|
| 5 |
+
RUN useradd -m -u 1000 user
|
| 6 |
+
USER root
|
| 7 |
+
|
| 8 |
+
ENV PIP_NO_CACHE_DIR=1 \
|
| 9 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 10 |
+
PYTHONUNBUFFERED=1 \
|
| 11 |
+
PATH="/home/user/.local/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
# Dependências nativas mínimas (pdf parsing)
|
| 14 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 15 |
+
build-essential poppler-utils \
|
| 16 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 17 |
+
|
| 18 |
+
WORKDIR /app
|
| 19 |
+
|
| 20 |
+
COPY --chown=user:user requirements.txt /app/requirements.txt
|
| 21 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 22 |
+
|
| 23 |
+
COPY --chown=user:user . /app
|
| 24 |
+
USER user
|
| 25 |
+
|
| 26 |
+
# Variáveis (podem ser sobrescritas nas "Variables" do Space)
|
| 27 |
+
ENV PORT=7860
|
| 28 |
+
ENV GROQ_MODEL_ID=deepseek-r1-distill-llama-70b
|
| 29 |
+
ENV TEMPERATURE=0.7
|
| 30 |
+
|
| 31 |
+
# O Space escuta em 7860
|
| 32 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -7,4 +7,26 @@ sdk: docker
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# RecrAI — API (FastAPI + Groq) para Análise de Currículos
|
| 11 |
+
|
| 12 |
+
API que analisa CVs (PDF ou texto) contra uma vaga e retorna JSON estruturado (inclui `score`), pronta para rodar em Hugging Face Spaces via Docker.
|
| 13 |
+
|
| 14 |
+
## Endpoints
|
| 15 |
+
|
| 16 |
+
### GET /health
|
| 17 |
+
Retorna status da API.
|
| 18 |
+
|
| 19 |
+
### POST /analyze_cv (multipart/form-data)
|
| 20 |
+
Campos:
|
| 21 |
+
- `job` (string) — descrição completa da vaga (obrigatório)
|
| 22 |
+
- `cv_text` (string) — texto do currículo (opcional se `file` for enviado)
|
| 23 |
+
- `file` (file/pdf) — PDF do currículo (opcional se `cv_text` for enviado)
|
| 24 |
+
|
| 25 |
+
### POST /analyze_cv_batch (application/json)
|
| 26 |
+
```json
|
| 27 |
+
{
|
| 28 |
+
"items": [
|
| 29 |
+
{ "job": "texto da vaga", "cv_text": "texto do cv" },
|
| 30 |
+
{ "job": "texto da vaga", "cv_pdf_b64": "<PDF em base64>" }
|
| 31 |
+
]
|
| 32 |
+
}
|
app.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uvicorn
|
| 3 |
+
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
|
| 4 |
+
from fastapi.responses import JSONResponse
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from typing import List, Optional
|
| 7 |
+
|
| 8 |
+
from models_schemas import AnalyzeResponse, AnalyzeBatchRequest
|
| 9 |
+
from llm_client import analyze_cv_with_llm
|
| 10 |
+
from parsers import extract_text_from_pdf
|
| 11 |
+
|
| 12 |
+
app = FastAPI(title="RecrAI API", version="1.0.0")
|
| 13 |
+
|
| 14 |
+
app.add_middleware(
|
| 15 |
+
CORSMiddleware,
|
| 16 |
+
allow_origins=["*"], allow_credentials=True,
|
| 17 |
+
allow_methods=["*"], allow_headers=["*"]
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
@app.get("/health")
|
| 21 |
+
def health():
|
| 22 |
+
return {"status": "ok"}
|
| 23 |
+
|
| 24 |
+
@app.post("/analyze_cv", response_model=AnalyzeResponse)
|
| 25 |
+
async def analyze_cv_endpoint(
|
| 26 |
+
job: str = Form(..., description="Descrição completa da vaga"),
|
| 27 |
+
cv_text: Optional[str] = Form(None, description="Texto do currículo (alternativa a PDF)"),
|
| 28 |
+
file: Optional[UploadFile] = File(None, description="Arquivo PDF do currículo")
|
| 29 |
+
):
|
| 30 |
+
if not cv_text and not file:
|
| 31 |
+
raise HTTPException(status_code=400, detail="Envie 'cv_text' ou 'file' (PDF).")
|
| 32 |
+
|
| 33 |
+
if file:
|
| 34 |
+
if not file.filename.lower().endswith(".pdf"):
|
| 35 |
+
raise HTTPException(status_code=415, detail="Apenas PDF é suportado no 'file'.")
|
| 36 |
+
pdf_bytes = await file.read()
|
| 37 |
+
cv_text = extract_text_from_pdf(pdf_bytes)
|
| 38 |
+
|
| 39 |
+
if not cv_text or not cv_text.strip():
|
| 40 |
+
raise HTTPException(status_code=422, detail="Não foi possível extrair texto do currículo.")
|
| 41 |
+
|
| 42 |
+
result = analyze_cv_with_llm(cv_text=cv_text, job_details=job)
|
| 43 |
+
return result
|
| 44 |
+
|
| 45 |
+
@app.post("/analyze_cv_batch", response_model=List[AnalyzeResponse])
|
| 46 |
+
async def analyze_cv_batch_endpoint(payload: AnalyzeBatchRequest):
|
| 47 |
+
results = []
|
| 48 |
+
for item in payload.items:
|
| 49 |
+
if not item.cv_text and not item.cv_pdf_b64:
|
| 50 |
+
raise HTTPException(status_code=400, detail="Cada item precisa de cv_text ou cv_pdf_b64.")
|
| 51 |
+
cv_text = item.cv_text
|
| 52 |
+
if not cv_text and item.cv_pdf_b64:
|
| 53 |
+
import base64
|
| 54 |
+
pdf_bytes = base64.b64decode(item.cv_pdf_b64)
|
| 55 |
+
cv_text = extract_text_from_pdf(pdf_bytes)
|
| 56 |
+
res = analyze_cv_with_llm(cv_text=cv_text, job_details=item.job)
|
| 57 |
+
results.append(res)
|
| 58 |
+
return results
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", "7860")))
|
data/jobs.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": 1,
|
| 4 |
+
"title": "Desenvolvedor(a) Full Stack Pleno",
|
| 5 |
+
"description": "Desenvolver e evoluir aplicações (front/back), integrações e CI/CD.",
|
| 6 |
+
"details": "React, Node, APIs REST, testes, boas práticas, cloud.",
|
| 7 |
+
"requirements": [
|
| 8 |
+
"React",
|
| 9 |
+
"Node",
|
| 10 |
+
"JavaScript",
|
| 11 |
+
"TypeScript",
|
| 12 |
+
"APIs REST",
|
| 13 |
+
"SQL",
|
| 14 |
+
"Docker"
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"id": 2,
|
| 19 |
+
"title": "Cientista de Dados Pleno",
|
| 20 |
+
"description": "Modelagem preditiva, EDA, métricas e MLOps.",
|
| 21 |
+
"details": "Pipelines, versionamento, documentação, comunicação.",
|
| 22 |
+
"requirements": [
|
| 23 |
+
"Pandas",
|
| 24 |
+
"Scikit-learn",
|
| 25 |
+
"Feature Engineering",
|
| 26 |
+
"Métricas",
|
| 27 |
+
"MLOps"
|
| 28 |
+
]
|
| 29 |
+
}
|
| 30 |
+
]
|
llm_client.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
from langchain_groq import ChatGroq
|
| 5 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 6 |
+
from models_schemas import AnalyzeResponse
|
| 7 |
+
from prompts import PROMPT_TEMPLATE, SCHEMA_JSON, PROMPT_SCORE
|
| 8 |
+
|
| 9 |
+
GROQ_MODEL_ID = os.getenv("GROQ_MODEL_ID", "deepseek-r1-distill-llama-70b")
|
| 10 |
+
TEMPERATURE = float(os.getenv("TEMPERATURE", "0.7"))
|
| 11 |
+
|
| 12 |
+
def _load_llm():
|
| 13 |
+
return ChatGroq(
|
| 14 |
+
model=GROQ_MODEL_ID,
|
| 15 |
+
temperature=TEMPERATURE,
|
| 16 |
+
max_tokens=None,
|
| 17 |
+
timeout=None,
|
| 18 |
+
max_retries=2,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
def _strip_think(text: str) -> str:
|
| 22 |
+
if "</think>" in text:
|
| 23 |
+
return text.split("</think>")[-1].strip()
|
| 24 |
+
return text.strip()
|
| 25 |
+
|
| 26 |
+
def _force_schema_fields(d: Dict[str, Any]) -> Dict[str, Any]:
|
| 27 |
+
keys = [
|
| 28 |
+
"name","area","summary","skills","education","interview_questions",
|
| 29 |
+
"strengths","areas_for_development","important_considerations",
|
| 30 |
+
"final_recommendations","score"
|
| 31 |
+
]
|
| 32 |
+
for k in keys:
|
| 33 |
+
if k not in d:
|
| 34 |
+
d[k] = [] if k in {
|
| 35 |
+
"skills","interview_questions","strengths",
|
| 36 |
+
"areas_for_development","important_considerations"
|
| 37 |
+
} else (0.0 if k == "score" else "")
|
| 38 |
+
return d
|
| 39 |
+
|
| 40 |
+
def analyze_cv_with_llm(cv_text: str, job_details: str) -> AnalyzeResponse:
|
| 41 |
+
llm = _load_llm()
|
| 42 |
+
prompt = ChatPromptTemplate.from_template(PROMPT_TEMPLATE)
|
| 43 |
+
chain = prompt | llm
|
| 44 |
+
output = chain.invoke({
|
| 45 |
+
"schema": SCHEMA_JSON,
|
| 46 |
+
"prompt_score": PROMPT_SCORE,
|
| 47 |
+
"cv": cv_text,
|
| 48 |
+
"job": job_details
|
| 49 |
+
})
|
| 50 |
+
|
| 51 |
+
raw = _strip_think(output.content)
|
| 52 |
+
|
| 53 |
+
start = raw.find("{")
|
| 54 |
+
end = raw.rfind("}")
|
| 55 |
+
if start == -1 or end == -1:
|
| 56 |
+
return AnalyzeResponse()
|
| 57 |
+
|
| 58 |
+
json_str = raw[start:end+1]
|
| 59 |
+
try:
|
| 60 |
+
data = json.loads(json_str)
|
| 61 |
+
except Exception:
|
| 62 |
+
return AnalyzeResponse()
|
| 63 |
+
|
| 64 |
+
data = _force_schema_fields(data)
|
| 65 |
+
return AnalyzeResponse(**data)
|
models_schemas.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
from pydantic import BaseModel, Field, validator
|
| 3 |
+
|
| 4 |
+
class AnalyzeResponse(BaseModel):
|
| 5 |
+
name: Optional[str] = ""
|
| 6 |
+
area: Optional[str] = ""
|
| 7 |
+
summary: Optional[str] = ""
|
| 8 |
+
skills: List[str] = Field(default_factory=list)
|
| 9 |
+
education: Optional[str] = ""
|
| 10 |
+
interview_questions: List[str] = Field(default_factory=list)
|
| 11 |
+
strengths: List[str] = Field(default_factory=list)
|
| 12 |
+
areas_for_development: List[str] = Field(default_factory=list)
|
| 13 |
+
important_considerations: List[str] = Field(default_factory=list)
|
| 14 |
+
final_recommendations: Optional[str] = ""
|
| 15 |
+
score: float = 0.0
|
| 16 |
+
|
| 17 |
+
@validator("score", pre=True, always=True)
|
| 18 |
+
def coerce_score(cls, v):
|
| 19 |
+
try:
|
| 20 |
+
return float(v)
|
| 21 |
+
except Exception:
|
| 22 |
+
return 0.0
|
| 23 |
+
|
| 24 |
+
class AnalyzeBatchItem(BaseModel):
|
| 25 |
+
job: str
|
| 26 |
+
cv_text: Optional[str] = None
|
| 27 |
+
cv_pdf_b64: Optional[str] = None
|
| 28 |
+
|
| 29 |
+
class AnalyzeBatchRequest(BaseModel):
|
| 30 |
+
items: List[AnalyzeBatchItem]
|
parsers.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
|
| 3 |
+
def extract_text_from_pdf(pdf_bytes: bytes) -> str:
|
| 4 |
+
"""
|
| 5 |
+
Tenta PyMuPDF (fitz). Se falhar, tenta pdfminer.six (fallback).
|
| 6 |
+
"""
|
| 7 |
+
text: Optional[str] = None
|
| 8 |
+
|
| 9 |
+
# 1) PyMuPDF
|
| 10 |
+
try:
|
| 11 |
+
import fitz # PyMuPDF
|
| 12 |
+
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
| 13 |
+
parts = []
|
| 14 |
+
for page in doc:
|
| 15 |
+
parts.append(page.get_text())
|
| 16 |
+
text = "\n".join(parts).strip()
|
| 17 |
+
except Exception:
|
| 18 |
+
text = None
|
| 19 |
+
|
| 20 |
+
# 2) pdfminer fallback
|
| 21 |
+
if not text:
|
| 22 |
+
try:
|
| 23 |
+
from io import BytesIO
|
| 24 |
+
from pdfminer.high_level import extract_text as pdfminer_extract
|
| 25 |
+
text = pdfminer_extract(BytesIO(pdf_bytes)).strip()
|
| 26 |
+
except Exception:
|
| 27 |
+
text = ""
|
| 28 |
+
|
| 29 |
+
return text or ""
|
prompts.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SCHEMA_JSON = """
|
| 2 |
+
{
|
| 3 |
+
"name": "Nome completo do candidato",
|
| 4 |
+
"area": "Área ou setor principal onde o candidato atua. Classifique em apenas uma: Desenvolvimento, Marketing, Vendas, Financeiro, Administrativo, Outros",
|
| 5 |
+
"summary": "Resumo objetivo sobre o perfil profissional do candidato",
|
| 6 |
+
"skills": ["competência 1", "competência 2", "..."],
|
| 7 |
+
"education": "Resumo da formação acadêmica mais relevante",
|
| 8 |
+
"interview_questions": ["Pelo menos 3 perguntas úteis para entrevista com base no currículo, para esclarecer algum ponto ou explorar melhor"],
|
| 9 |
+
"strengths": ["Pontos fortes e aspectos que indicam alinhamento com o perfil ou vaga desejada"],
|
| 10 |
+
"areas_for_development": ["Pontos que indicam possíveis lacunas, fragilidades ou necessidades de desenvolvimento"],
|
| 11 |
+
"important_considerations": ["Observações específicas que merecem verificação ou cuidado adicional"],
|
| 12 |
+
"final_recommendations": "Resumo avaliativo final com sugestões de próximos passos (ex: seguir com entrevista, indicar para outra vaga)",
|
| 13 |
+
"score": 0.0
|
| 14 |
+
}
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
PROMPT_SCORE = """
|
| 18 |
+
Com base na vaga específica, calcule a pontuação final (de 0.0 a 10.0).
|
| 19 |
+
O retorno para esse campo deve conter apenas a pontuação final (x.x) sem mais nenhum texto ou anotação.
|
| 20 |
+
Seja justo e rigoroso ao atribuir as notas. A nota 10.0 só deve ser atribuída para candidaturas que superem todas as expectativas da vaga.
|
| 21 |
+
|
| 22 |
+
Critérios de avaliação:
|
| 23 |
+
1. Experiência (Peso: 35% do total): Análise de posições anteriores, tempo de atuação e similaridade com as responsabilidades da vaga.
|
| 24 |
+
2. Habilidades Técnicas (Peso: 25% do total): Verifique o alinhamento das habilidades técnicas com os requisitos mencionados na vaga.
|
| 25 |
+
3. Educação (Peso: 15% do total): Avalie a relevância da graduação/certificações para o cargo, incluindo instituições e anos de estudo.
|
| 26 |
+
4. Pontos Fortes (Peso: 15% do total): Avalie a relevância dos pontos fortes (ou alinhamentos) para a vaga.
|
| 27 |
+
5. Pontos Fracos (Desconto de até 10%): Avalie a gravidade dos pontos fracos (ou desalinhamentos) para a vaga.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
PROMPT_TEMPLATE = """
|
| 31 |
+
Você é um especialista em Recursos Humanos com vasta experiência em análise de currículos.
|
| 32 |
+
Sua tarefa é analisar o conteúdo a seguir e extrair os dados conforme o formato abaixo, para cada um dos campos.
|
| 33 |
+
Responda apenas com o JSON estruturado e utilize somente essas chaves. Cuide para que os nomes das chaves sejam exatamente esses.
|
| 34 |
+
Não adicione explicações ou anotações fora do JSON.
|
| 35 |
+
Schema desejado:
|
| 36 |
+
{schema}
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
Para o cálculo do campo score:
|
| 40 |
+
{prompt_score}
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
Currículo a ser analisado:
|
| 45 |
+
'{cv}'
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
Vaga que o candidato está se candidatando:
|
| 50 |
+
'{job}'
|
| 51 |
+
"""
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn[standard]==0.30.6
|
| 3 |
+
python-multipart==0.0.9
|
| 4 |
+
|
| 5 |
+
pydantic==2.9.2
|
| 6 |
+
langchain==0.2.16
|
| 7 |
+
langchain-groq==0.1.5
|
| 8 |
+
|
| 9 |
+
PyMuPDF==1.24.11
|
| 10 |
+
pdfminer.six==20231228
|
tests/sample.http
ADDED
|
File without changes
|