diff --git a/fowoco-knowledge/hr-intent-service/.dockerignore b/fowoco-knowledge/hr-intent-service/.dockerignore new file mode 100644 index 0000000..1de7099 --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/.dockerignore @@ -0,0 +1,8 @@ +venv/ +__pycache__/ +*.pyc +.env +.git/ +.gitignore +README.md +server.log \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/.env.example b/fowoco-knowledge/hr-intent-service/.env.example new file mode 100644 index 0000000..1116141 --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/.env.example @@ -0,0 +1,11 @@ +BERT_MODEL_DIR=fowoco/klue-roberta-base-intent-classifier +AX_BASE_MODEL_NAME=skt/A.X-4.0-Light +AX_ADAPTER_PATH=fowoco/ax-intent-qlora +HF_TOKEN= +MARGIN_THRESHOLD=0.76 +MAX_TRAINED_LABELS=3 +LABEL_PROB_THRESHOLD=0.55 +MAX_INPUT_LENGTH=150 +AX_MAX_NEW_TOKENS=96 +DEVICE=auto +ENABLE_AX=True \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/.gitignore b/fowoco-knowledge/hr-intent-service/.gitignore new file mode 100644 index 0000000..806dcaf --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/.gitignore @@ -0,0 +1,6 @@ +venv/ +__pycache__/ +*.pyc +.env +server.log +models/ \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/Dockerfile b/fowoco-knowledge/hr-intent-service/Dockerfile new file mode 100644 index 0000000..aa4ab89 --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/Dockerfile @@ -0,0 +1,18 @@ +FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 + +RUN apt-get update && apt-get install -y python3.11 python3-pip && rm -rf /var/lib/apt/lists/* + +WORKDIR /srv + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app + +# 모델은 HF Hub에서 받아온다. +ENV BERT_MODEL_DIR=fowoco/klue-roberta-base-intent-classifier +ENV AX_BASE_MODEL_NAME=skt/A.X-4.0-Light +ENV AX_ADAPTER_PATH=fowoco/ax-intent-qlora + +EXPOSE 8000 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/README.md b/fowoco-knowledge/hr-intent-service/README.md new file mode 100644 index 0000000..c1adfe9 --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/README.md @@ -0,0 +1,82 @@ +# HR Intent Classification model + +BERT(Full FT) 메인 모델 + A.X-4.0-Light(QLoRA) 보조 모델 cascade 구조의 HR 업무 요청 문장 Intent 분류 모델 + +## 현재 상태 (2026-08-04 기준) + +- ✅ 모델 학습·검증 완료 +- ✅ 로컬 FastAPI 서비스 구현 및 검증 완료 +- ✅ Hugging Face Hub(private) 모델 저장소 연동 완료 +- ✅ Colab GPU 환경에서 BERT + A.X 전체 cascade 실제 요청/응답 검증 완료 + + +## 아키텍처 + +```text +[HR 입력 데이터 수신] + │ + ▼ +[1차 검증] 활성 라벨 수 ≥ 3개? (OOD) ────────► (YES) ──┐ + │ │ + ▼ │ +[2차 검증] 고위험/오답 키워드 포함? ─────────► (YES) ──┼──► [A.X-4.0-Light (LLM) 호출] + │ │ + ▼ │ +[3차 검증] Margin Score < 0.76 ? ────────────► (YES) ──┘ + │ + └── (NO: 모든 안전망 통과) ──────────────────► [BERT 결과 최종 사용] +``` + +라우팅 규칙 및 모델 구조 선정 근거는 팀 노션 문서 참조 + +### 모델 구성 + +| 모델 | 역할 | 방식 | Validation 268건 정확도 | +|---|---|---|---| +| klue/roberta-base | 메인 | Full Fine-tuning | 95.5% | +| A.X-4.0-Light | 보조 | QLoRA (checkpoint-402) | 92.2% | +| Cascade 모델 | 최종 | 메인 모델 + 보조 모델 , 라우팅 조건 적용 | 93.2% | + + +## Hugging Face Hub 연동 + +https://huggingface.co/fowoco + +모델 학습 가중치는 `fowoco` 조직의 private repo에 저장되어 있다. GitHub에는 코드만 올리고, 모델 파일은 여기서 관리한다 . +``` +fowoco/klue-roberta-base-intent-classifier +fowoco/ax-intent-qlora +``` + +### 필요한 환경변수 + +``` +BERT_MODEL_DIR=fowoco/klue-roberta-base-intent-classifier +AX_BASE_MODEL_NAME=skt/A.X-4.0-Light # 공개 모델, 토큰 불필요 +AX_ADAPTER_PATH=fowoco/ax-intent-qlora +HF_TOKEN= +``` + +`HF_TOKEN`은 절대 코드나 `Dockerfile`에 하드코딩하지 않는다. `.env`(`.gitignore`로 제외됨) 또는 배포 시 Secret으로 주입한다. + + +## 로컬 실행 - 가상환경 + +```bash +python -m venv venv +venv\Scripts\activate # Windows +pip install -r requirements.txt +cp .env.example .env # 값 채우기 (HF_TOKEN 등) +uvicorn app.main:app --reload +``` + +GPU가 없는 로컬 환경에서는 `.env`에 `ENABLE_AX=False`로 두면 BERT만으로 서비스가 뜬다 (A.X 로드 실패 시에도 동일하게 자동으로 BERT-only degraded 모드로 전환됨, `pipeline.py` 참고). + +## 로컬 실행 - Docker + +```bash +docker build -t hr-intent-service:test . +docker run -p 8000:8000 --env-file .env hr-intent-service:test +``` + +로컬 환경에서는 `.env`에 `ENABLE_AX=False`로 둘 것을 권장함. \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/app/__init__.py b/fowoco-knowledge/hr-intent-service/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fowoco-knowledge/hr-intent-service/app/ax_model.py b/fowoco-knowledge/hr-intent-service/app/ax_model.py new file mode 100644 index 0000000..53207bd --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/app/ax_model.py @@ -0,0 +1,96 @@ +"""A.X-4.0-Light QLoRA 파인튜닝 모델 로드 및 추론.""" + +import json +import re + +import torch +from peft import PeftModel +from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig + +SYSTEM_PROMPT = """당신은 HR 업무 요청 문장(hr_input)을 분석하여 의도(Intent)를 분류하는 전문 AI 에이전트입니다. +Intent 모델의 책임은 Intent + evidence 추출까지입니다. Workflow 선택, Slot 수집, 외부기관 제출, 법적 판단, 업무 실행 여부는 이 모델의 책임이 아닙니다. + +### 1. Intent 정의 (7개) +1. WORK_INSTRUCTION: 작업 지시, 근무 일정 변경, 현장 행동 안내 +2. DOCUMENT_REQUEST: 여권/등록증/계약서/증명서 등 서류를 받거나 제출을 요청·추적하는 행위 자체 +3. PAYROLL_EXPLANATION: 급여, 수당, 공제 내역, 출퇴근/근태 관련 설명·문의 (급여계좌 등록/변경은 제외 → WORKER_ONBOARDING) +4. WORKER_ONBOARDING: 신규 입사자 등록, 보험 최초 가입, 초기 프로필·급여계좌 등록 (서류가 이미 있는 상태에서의 처리) +5. EMPLOYMENT_CHANGE: 휴가, 퇴사, 무단결근/연락두절, 사업장 변경 등 재직 상태 변동 확인·신고 +6. EXPIRY_RENEWAL: 근로계약, 체류기간, 고용허가기간 등 만료 임박·연장·갱신 절차 +7. OUT_OF_SCOPE: 위 6개 외 HR 범주 밖 요청, 또는 새 실행 요청 없이 결과만 보고하는 문장. 다른 Intent와 병행 불가 + +### 2. 핵심 판별 규칙 +- 규칙 A: 최종 목적이 아니라 발화문에서 지금 당장 실행을 요구하는 행위로 판단합니다. +- 규칙 B: "받아서/제출받아/요청해/첨부해줘" 등 서류 확보 표현이 명시적으로 있을 때만 DOCUMENT_REQUEST를 부착합니다. +- 규칙 C: 여러 Intent가 있으면 발화문 등장 순서대로 배열합니다. OUT_OF_SCOPE는 단독으로만 존재합니다. +- 규칙 D: evidence는 원문 문자를 그대로(exact substring) 추출합니다. OUT_OF_SCOPE는 evidence: null입니다. + +### 3. 경계 규칙 +- 완료/상태 보고 문장은 OUT_OF_SCOPE, 요청형이면 원래 Intent 유지. +- 휴가는 명시적 액션이면 EMPLOYMENT_CHANGE, 배경절이면 제외. +- 급여계좌 등록/변경은 WORKER_ONBOARDING, 순수 급여 설명/문의는 PAYROLL_EXPLANATION. + +### 4. 출력 형식 +다른 설명, 마크다운, 코드블록 없이 오직 아래 JSON 형식 텍스트만 출력합니다: +{"intents": [{"intent": "INTENT_CODE", "evidence": "원문에서 추출한 정확한 부분 문자열 또는 null"}]} + +이제 아래 입력 문장을 위 규칙에 따라 JSON 형식으로만 분류하십시오.""" + + +def _extract_json(text: str) -> dict | None: + match = re.search(r"\{.*\}", text, re.DOTALL) + if not match: + return None + try: + return json.loads(match.group(0)) + except json.JSONDecodeError: + return None + + +class AxIntentModel: + def __init__( + self, base_model_name: str, adapter_path: str, device: str, max_new_tokens: int = 96, hf_token : str | None = None, + ): + self.max_new_tokens = max_new_tokens + bnb_config = BitsAndBytesConfig( + load_in_4bit=True, + bnb_4bit_quant_type="nf4", + bnb_4bit_compute_dtype=torch.float16, + ) + base_model = AutoModelForCausalLM.from_pretrained( + base_model_name, + quantization_config=bnb_config, + torch_dtype=torch.float16, + device_map={"": 0} if device != "cpu" else "cpu", + token = hf_token, + ) + self.tokenizer = AutoTokenizer.from_pretrained(base_model_name, token=hf_token) + self.model = PeftModel.from_pretrained(base_model, adapter_path, token=hf_token) + self.model.eval() + + def predict(self, hr_input: str) -> list[dict]: + """실패 시 예외를 던진다 — 호출부(pipeline)에서 graceful degradation 처리.""" + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": hr_input}, + ] + inputs = self.tokenizer.apply_chat_template( + messages, + add_generation_prompt=True, + tokenize=True, + return_dict=True, + return_tensors="pt", + ).to(self.model.device) + + with torch.no_grad(): + output = self.model.generate( + **inputs, max_new_tokens=self.max_new_tokens, do_sample=False + ) + + raw = self.tokenizer.decode( + output[0][inputs["input_ids"].shape[-1] :], skip_special_tokens=True + ) + parsed = _extract_json(raw) + if parsed is None: + raise ValueError(f"A.X output could not be parsed as JSON: {raw!r}") + return parsed.get("intents", []) \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/app/bert_model.py b/fowoco-knowledge/hr-intent-service/app/bert_model.py new file mode 100644 index 0000000..991413a --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/app/bert_model.py @@ -0,0 +1,43 @@ +"""klue/roberta-base Full Fine-tuning 모델 로드 및 추론.""" + +import torch +from transformers import AutoModelForSequenceClassification, AutoTokenizer + + +class BertIntentModel: + def __init__(self, model_dir: str, device: str, label_prob_threshold: float = 0.55, hf_token: str | None = None, ): + self.device = "cuda" if (device == "auto" and torch.cuda.is_available()) else ( + device if device != "auto" else "cpu" + ) + self.tokenizer = AutoTokenizer.from_pretrained(model_dir, token=hf_token) + self.model = AutoModelForSequenceClassification.from_pretrained(model_dir, token=hf_token) + self.model.to(self.device).eval() + self.id2label = self.model.config.id2label + self.label_prob_threshold = label_prob_threshold + + @torch.no_grad() + def predict(self, text: str) -> tuple[dict[str, float], float, list[str]]: + """확률 딕셔너리, margin, 활성화된 intent 리스트를 반환. + + margin: 활성화(threshold 이상)된 것 중 최저 확률 - 비활성화된 것 중 최고 확률. + """ + enc = self.tokenizer(text, truncation=True, max_length=64, return_tensors="pt").to( + self.device + ) + logits = self.model(**enc).logits + probs_array = torch.sigmoid(logits)[0].cpu().numpy() + probs_dict = {self.id2label[i]: float(p) for i, p in enumerate(probs_array)} + + activated = [p for p in probs_array if p >= self.label_prob_threshold] + not_activated = [p for p in probs_array if p < self.label_prob_threshold] + + if not activated: + margin = float(max(probs_array)) - self.label_prob_threshold + else: + margin = float(min(activated)) - (float(max(not_activated)) if not_activated else 0.0) + + picked = [self.id2label[i] for i, p in enumerate(probs_array) if p >= self.label_prob_threshold] + if not picked: + picked = [self.id2label[int(probs_array.argmax())]] + + return probs_dict, margin, picked \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/app/config.py b/fowoco-knowledge/hr-intent-service/app/config.py new file mode 100644 index 0000000..26b12eb --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/app/config.py @@ -0,0 +1,44 @@ +"""서비스 설정. 모든 값은 환경변수로 주입. + +로컬 개발: .env 파일 사용 +운영 배포: 컨테이너 오케스트레이터 주입 +""" + +from functools import lru_cache + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") + + # 모델 경로 - Hugging Face Hub repo ID 형식 + # BERT/A.X 어댑터는 fowoco 조직의 private repo이므로 hf_token 인증이 필요 + # A.X 베이스 모델(skt/A.X-4.0-Light)은 공개 모델이라 토큰 없이도 접근 가능 + + bert_model_dir: str = "fowoco/klue-roberta-base-intent-classifier" + ax_base_model_name: str = "skt/A.X-4.0-Light" + ax_adapter_path: str = "fowoco/ax-intent-qlora" + + hf_token: str | None = None + + # 라우팅 규칙 파라미터 + margin_threshold: float = 0.76 + max_trained_labels: int = 3 + label_prob_threshold: float = 0.55 + + # 입력 검증 + max_input_length: int = 150 + + # 생성 파라미터 (A.X) + ax_max_new_tokens: int = 96 + + # 런타임 + device: str = "auto" # "auto" | "cuda" | "cpu" + enable_ax: bool = True + + +@lru_cache +def get_settings() -> Settings: + """설정을 한 번만 로드하고 재사용 (매 요청마다 다시 읽지 않음).""" + return Settings() \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/app/guardrail.py b/fowoco-knowledge/hr-intent-service/app/guardrail.py new file mode 100644 index 0000000..cffc067 --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/app/guardrail.py @@ -0,0 +1,77 @@ +"""BERT 예측을 A.X로 넘길지 결정하는 라우팅 규칙. + +각 조건의 근거는 노션 참조. +""" + +from dataclasses import dataclass, field + + +@dataclass +class RoutingResult: + should_route: bool + reason: str + category: str + + +@dataclass +class HRRoutingGuardrail: + margin_threshold: float = 0.76 + max_trained_labels: int = 3 + label_prob_threshold: float = 0.55 + + status_kw: list[str] = field( + default_factory=lambda: ["없음", "완료", "이상없", "특이사항", "특이문의"] + ) + action_kw: list[str] = field(default_factory=lambda: ["배치", "라인", "지시"]) + doc_kw: list[str] = field( + default_factory=lambda: ["신청서", "서류", "챙겨", "접수", "명단확인"] + ) + + @staticmethod + def _normalize(text: str) -> str: + return text.replace(" ", "") + + def should_route_to_ax( + self, hr_input: str, probs: dict[str, float], margin: float + ) -> RoutingResult: + clean_input = self._normalize(hr_input) + + activated_count = sum(1 for p in probs.values() if p >= self.label_prob_threshold) + if activated_count >= self.max_trained_labels: + return RoutingResult( + should_route=True, + reason=f"활성 label {activated_count}개 (학습 최댓값 {self.max_trained_labels}개 이상)", + category="OOD_Label_Count", + ) + + if any(kw in clean_input for kw in self.status_kw): + return RoutingResult( + should_route=True, reason="완료/상태 보고 키워드 감지", category="Rule_Status" + ) + + action_matches = sum(1 for kw in self.action_kw if kw in clean_input) + if action_matches >= 2: + return RoutingResult( + should_route=True, + reason=f"배치/라인/지시 키워드 {action_matches}개 감지", + category="Rule_Action", + ) + + if "급여계좌" in clean_input or ("급여" in clean_input and "확인" in clean_input): + return RoutingResult( + should_route=True, reason="급여계좌 관련 경계 키워드 감지", category="Rule_Salary" + ) + + if any(kw in clean_input for kw in self.doc_kw): + return RoutingResult( + should_route=True, reason="서류 확보 키워드 감지", category="Rule_Document" + ) + + if margin < self.margin_threshold: + return RoutingResult( + should_route=True, + reason=f"margin {margin:.3f} < {self.margin_threshold}", + category="Low_Margin", + ) + + return RoutingResult(should_route=False, reason="BERT 신뢰 구간 통과", category="Pass_BERT") \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/app/main.py b/fowoco-knowledge/hr-intent-service/app/main.py new file mode 100644 index 0000000..0d61647 --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/app/main.py @@ -0,0 +1,34 @@ +"""서비스 진입점. uvicorn app.main:app 으로 실행.""" + +import logging + +from fastapi import FastAPI + +from .config import get_settings +from .pipeline import HybridIntentPipeline +from .schema import ClassifyRequest, ClassifyResponse + +logging.basicConfig(level=logging.INFO) + +app = FastAPI(title="HR Intent Classification Service") +pipeline: HybridIntentPipeline | None = None + + +@app.on_event("startup") +def load_models() -> None: + global pipeline + settings = get_settings() + pipeline = HybridIntentPipeline(settings) + + +@app.get("/health") +def health() -> dict: + return { + "status": "ok" if pipeline is not None else "loading", + "ax_available": pipeline.ax_available if pipeline else False, + } + + +@app.post("/api/v1/intents/classify", response_model=ClassifyResponse) +def classify(request: ClassifyRequest) -> ClassifyResponse: + return pipeline.predict(request.instruction) \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/app/pipeline.py b/fowoco-knowledge/hr-intent-service/app/pipeline.py new file mode 100644 index 0000000..6c1fcef --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/app/pipeline.py @@ -0,0 +1,89 @@ +"""BERT + 가드레일 + A.X를 조합하는 하이브리드 추론 파이프라인.""" + +import logging +import time + +from .ax_model import AxIntentModel +from .bert_model import BertIntentModel +from .config import Settings +from .guardrail import HRRoutingGuardrail +from .schema import ClassifyResponse, format_ax_output, format_bert_output + +logger = logging.getLogger(__name__) + + +class HybridIntentPipeline: + def __init__(self, settings: Settings): + self.settings = settings + + logger.info("Loading BERT model from %s", settings.bert_model_dir) + self.bert = BertIntentModel( + model_dir=settings.bert_model_dir, + device=settings.device, + label_prob_threshold=settings.label_prob_threshold, + hf_token=settings.hf_token, + ) + + self.guardrail = HRRoutingGuardrail( + margin_threshold=settings.margin_threshold, + max_trained_labels=settings.max_trained_labels, + label_prob_threshold=settings.label_prob_threshold, + ) + + # A.X는 선택적으로 로드한다 — 실패해도 BERT만으로 서비스가 뜨도록 한다. + self.ax: AxIntentModel | None = None + if settings.enable_ax : + try: + logger.info("Loading A.X model (adapter: %s)", settings.ax_adapter_path) + self.ax = AxIntentModel( + base_model_name=settings.ax_base_model_name, + adapter_path=settings.ax_adapter_path, + device=settings.device, + max_new_tokens=settings.ax_max_new_tokens, + hf_token=settings.hf_token, + ) + except Exception: + logger.exception( + "A.X model failed to load. Service starts in BERT-only degraded mode." + ) + else : + logger.info("A.X disabled via settings (enable_ax=False). BERT-only mode.") + + @property + def ax_available(self) -> bool: + return self.ax is not None + + def predict(self, instruction: str) -> ClassifyResponse: + start = time.perf_counter() + probs, margin, bert_intents = self.bert.predict(instruction) + route = self.guardrail.should_route_to_ax(instruction, probs, margin) + + if route.should_route and self.ax_available: + try: + ax_intents = self.ax.predict(instruction) + return format_ax_output( + hr_input=instruction, + ax_intents=ax_intents, + all_scores=probs, + margin=margin, + device=self.bert.device, + start_time=start, + routing_category=route.category, + routing_reason=route.reason, + ) + except Exception: + logger.exception("A.X inference failed for input, falling back to BERT") + # 아래로 흘러서 degraded BERT 응답 반환 + + degraded = route.should_route # 넘겨야 했는데 A.X를 못 쓴 경우만 True + return format_bert_output( + hr_input=instruction, + bert_intents=bert_intents, + all_scores=probs, + margin=margin, + device=self.bert.device, + start_time=start, + degraded=degraded, + routing_category=route.category, + routing_reason=route.reason, + ) \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/app/schema.py b/fowoco-knowledge/hr-intent-service/app/schema.py new file mode 100644 index 0000000..c81ebe9 --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/app/schema.py @@ -0,0 +1,96 @@ +"""BERT와 A.X의 서로 다른 출력 구조(evidence 유무, score 유무)를 하나의 응답 스키마로 통합한다.""" + +import time +from typing import Literal + +from pydantic import BaseModel, Field + + +class ClassifyRequest(BaseModel): + instruction: str = Field(..., min_length=1, max_length=150) + + +class IntentItem(BaseModel): + intent: str + evidence: str | None = None # BERT는 항상 None, A.X는 문자열 또는 None(OUT_OF_SCOPE) + score: float | None = None # BERT는 sigmoid 확률, A.X는 확률을 안 내므로 None + + +class ResponseMeta(BaseModel): + selected_model: Literal["BERT", "AX", "BERT_FALLBACK"] + routing_category: str + routing_reason: str + degraded: bool # A.X 라우팅 대상이었으나 A.X 호출 실패로 BERT 결과를 대신 반환한 경우 + bert_margin: float + bert_all_scores: dict[str, float] # 항상 기록 (A.X로 넘어간 경우도 라우팅 재검증용으로 필요) + device: str + latency_ms: float + + +class ClassifyResponse(BaseModel): + input: str + intents: list[IntentItem] + meta: ResponseMeta + + +def format_bert_output( + hr_input: str, + bert_intents: list[str], + all_scores: dict[str, float], + margin: float, + device: str, + start_time: float, + degraded: bool = False, + routing_category: str = "Pass_BERT", + routing_reason: str = "BERT 신뢰 구간 통과", +) -> ClassifyResponse: + return ClassifyResponse( + input=hr_input, + intents=[ + IntentItem(intent=name, evidence=None, score=round(all_scores[name], 4)) + for name in bert_intents + ], + meta=ResponseMeta( + selected_model="BERT_FALLBACK" if degraded else "BERT", + routing_category=routing_category, + routing_reason=routing_reason, + degraded=degraded, + bert_margin=round(margin, 4), + bert_all_scores={k: round(v, 4) for k, v in all_scores.items()}, + device=device, + latency_ms=round((time.perf_counter() - start_time) * 1000, 1), + ), + ) + + +def format_ax_output( + hr_input: str, + ax_intents: list[dict], + all_scores: dict[str, float], + margin: float, + device: str, + start_time: float, + routing_category: str, + routing_reason: str, +) -> ClassifyResponse: + return ClassifyResponse( + input=hr_input, + intents=[ + IntentItem( + intent=item["intent"], + evidence=item.get("evidence"), + score=None, + ) + for item in ax_intents + ], + meta=ResponseMeta( + selected_model="AX", + routing_category=routing_category, + routing_reason=routing_reason, + degraded=False, + bert_margin=round(margin, 4), + bert_all_scores={k: round(v, 4) for k, v in all_scores.items()}, + device=device, + latency_ms=round((time.perf_counter() - start_time) * 1000, 1), + ), + ) \ No newline at end of file diff --git a/fowoco-knowledge/hr-intent-service/requirements.txt b/fowoco-knowledge/hr-intent-service/requirements.txt new file mode 100644 index 0000000..e11eb9a --- /dev/null +++ b/fowoco-knowledge/hr-intent-service/requirements.txt @@ -0,0 +1,8 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +pydantic-settings>=2.5.0 +transformers>=4.46.0 +accelerate +bitsandbytes +peft +torch \ No newline at end of file