From 7b3b299cbb35905b8aa6c0a3be224dfe418c1d18 Mon Sep 17 00:00:00 2001 From: bangae1 Date: Wed, 28 Jan 2026 15:06:02 +0900 Subject: [PATCH] first --- .gitignore | 5 + README.md | 31 ++ __pycache__/org_offline.cpython-312.pyc | Bin 0 -> 11367 bytes .../org_transformer_offline.cpython-312.pyc | Bin 0 -> 2051 bytes download_embed.py | 6 + org.html | 127 ++++++++ org_offline.py | 297 ++++++++++++++++++ org_transformer_offline.py | 57 ++++ requirements.txt | Bin 0 -> 4184 bytes test.py | 5 + vector_transformer.py | 55 ++++ vector_transformer_offline.py | 59 ++++ 12 files changed, 642 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __pycache__/org_offline.cpython-312.pyc create mode 100644 __pycache__/org_transformer_offline.cpython-312.pyc create mode 100644 download_embed.py create mode 100644 org.html create mode 100644 org_offline.py create mode 100644 org_transformer_offline.py create mode 100644 requirements.txt create mode 100644 test.py create mode 100644 vector_transformer.py create mode 100644 vector_transformer_offline.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa74f9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.venv +.idea +chroma_db +models +*.iml \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcb1296 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# 인사정보 AI + +# pip offline download +pip download -r requirements.txt --no-binary :all: -d /path/to/download/dir + +# Qwen/Qwen3-0.6B model download +HF CLI 설치 (처음 한 번만) + +pip install huggingface_hub + +로그인 (오프라인 사용 전 한 번만 필요) + +huggingface-cli login # 토큰 입력 (https://huggingface.co/settings/tokens) + +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 + +모델 로컬 저장 + +huggingface-cli download Qwen/Qwen3-0.6B --local-dir ./models/Qwen3-0.6B --local-dir-use-symlinks False + +# sentence-transformers model download (chroma vector db) +from sentence_transformers import SentenceTransformer + +model = SentenceTransformer('all-MiniLM-L6-v2') + +model.save('./models/all-MiniLM-L6-v2') + +# 실행방법 +uvicorn manual:app --host 0.0.0.0 --port 8040 --reload + + \ No newline at end of file diff --git a/__pycache__/org_offline.cpython-312.pyc b/__pycache__/org_offline.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48bd3126f369694d04cffa51682cf49f06bbee8b GIT binary patch literal 11367 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 literal 0 HcmV?d00001 diff --git a/download_embed.py b/download_embed.py new file mode 100644 index 0000000..f67f872 --- /dev/null +++ b/download_embed.py @@ -0,0 +1,6 @@ +from sentence_transformers import SentenceTransformer + +# 모델 다운로드 및 로컬 저장 +# 이 스크립트는 인터넷 연결이 필요하며, 한 번 실행 후에는 오프라인에서 모델을 사용할 수 있게 해줍니다. +model = SentenceTransformer('jhgan/ko-sroberta-multitask') +model.save('./models/ko-sroberta-multitask') diff --git a/org.html b/org.html new file mode 100644 index 0000000..9f57b14 --- /dev/null +++ b/org.html @@ -0,0 +1,127 @@ + + + + + Title + + + +
+
+
+ +
+
+ + + \ No newline at end of file diff --git a/org_offline.py b/org_offline.py new file mode 100644 index 0000000..0e31339 --- /dev/null +++ b/org_offline.py @@ -0,0 +1,297 @@ +from threading import Thread +import json +from typing import List, Generator + +import torch +import chromadb +from pydantic import BaseModel +from starlette.middleware.cors import CORSMiddleware +from starlette.responses import StreamingResponse +from fastapi import FastAPI +from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer + +from org_transformer_offline import init + +# === 경로 설정 (모두 로컬) === +QWEN_MODEL_PATH = "./models/Qwen3-0.6B" + +# 전역 변수 설정 +_model = None +_tokenizer = None + +# 2. 벡터 DB 설정 +persist_directory = "./chroma_db" +chroma_client = chromadb.PersistentClient(path=persist_directory) + +collection = chroma_client.get_or_create_collection( + name="orgchart", +) + + +def search_employees(data: List[dict], query: str) -> List[dict]: + """ + 직원 데이터에서 검색어가 포함된 항목을 필터링합니다. + (현재 API에서 직접 사용되지 않으나 유틸리티 목적으로 유지) + + Args: + data (List[dict]): 직원 데이터 리스트 + query (str): 검색어 + + Returns: + List[dict]: 필터링된 직원 리스트 + """ + if not query: + return data + + query = query.lower().strip() + + # 모든 필드값 중 검색어가 포함된 항목 필터링 + filtered = [ + emp for emp in data + if any(query in str(value).lower() for value in emp.values() if value) + ] + return filtered + + +def get_qwen_model(): + """ + Qwen 모델과 토크나이저를 로드하거나 캐시된 인스턴스를 반환합니다. + torch.compile을 사용하여 추론 속도를 최적화합니다. + + Returns: + tuple: (model, tokenizer) + """ + global _model, _tokenizer + if _model is not None: + return _model, _tokenizer + + # 토크나이저 로드 + _tokenizer = AutoTokenizer.from_pretrained( + QWEN_MODEL_PATH, + trust_remote_code=True, + local_files_only=True + ) + + # 모델 로드 + _model = AutoModelForCausalLM.from_pretrained( + QWEN_MODEL_PATH, + dtype=torch.bfloat16, # CPU: bfloat16, GPU: float16 권장 + device_map="auto", + trust_remote_code=True, + local_files_only=True + ) + + # ✅ torch.compile() 적용 (PyTorch 2.0+) + if hasattr(torch, 'compile'): + try: + print("🚀 torch.compile() 적용 중...") + # mode="reduce-overhead": 추론 시 추천 + # dynamic=True: 입력 길이가 유동적인 RAG 환경에 적합 + _model = torch.compile( + _model, + mode="reduce-overhead", + dynamic=True + ) + print("✅ torch.compile() 성공!") + except Exception as e: + print(f"⚠️ torch.compile() 실패, 원본 모델 사용: {e}") + pass # 실패하면 원본 사용 + return _model, _tokenizer + + +def query_and_summarize_stream(sessionId: str, query: str, top_k: int = 3, min_similarity: float = 0.2) -> Generator: + """ + 사용자 질문에 대해 벡터 DB를 검색하고, LLM을 통해 답변을 스트리밍으로 생성합니다. + + Args: + sessionId (str): 세션 ID (사용자 구분) + query (str): 사용자 질문 + top_k (int): 검색할 문서 개수 + min_similarity (float): 최소 유사도 임계값 + + Returns: + Generator: 스트리밍 응답 제너레이터 + """ + from datetime import datetime + + # 관련 문서 검색 (top_k보다 여유 있게 가져옴) + results = collection.query( + query_texts=[query], + n_results=top_k + 2, # 여유분 확보 + where={"sessionId": sessionId} + ) + + print(f"검색 결과: {results}") + + if not results['documents'] or not results['documents'][0]: + def generate_empty(): + yield json.dumps({"kind": "text", "text": "관련 문서를 찾을 수 없습니다."}) + "\n" + return generate_empty + + # 유사도 계산 및 필터링 + filtered_docs = [] + if results['distances'] and results['distances'][0]: + for doc, dist in zip(results['documents'][0], results['distances'][0]): + similarity = 1 - dist + if similarity >= min_similarity: + filtered_docs.append((doc, similarity)) + if len(filtered_docs) >= top_k: + break + print(f"필터링된 문서: {filtered_docs}") + + if not filtered_docs: + def generate_low_sim(): + yield json.dumps({"kind": "text", "text": "유사도 기준을 만족하는 문서가 없습니다."}) + "\n" + return generate_low_sim + + # 컨텍스트 생성 + context_parts = [] + for i, (doc, sim) in enumerate(filtered_docs): + context_parts.append(f"[청크 {i+1} | 유사도: {sim:.3f}]\n{doc}") + context = "\n\n".join(context_parts) + + # 모델 로드 + model, tokenizer = get_qwen_model() + + sub_query = '' + if query.find('부') > -1: + sub_query = '***부로 끝나는 단어는 부서 맵핑' + + # 프롬프트 구성 + messages = [ + { + "role": "system", + "content": ( + ''' + 당신은 인사 담당 어시스던트 입니다. 인사 이동, 승진, 적정 부서 이동 등 전반적으로 모든 인사 정보에 대해 답변해야합니다. + hint) {} + '''.format(sub_query) + ) + }, + { + "role": "user", + "content": f"다음 데이터를 참고하세요:\n\n{context}\n\n질문: {query}" + } + ] + + print(f'Messages: {messages}') + + # 토큰화 + text = tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True, + enable_thinking=False # Qwen 모델 버전에 따라 지원 여부 확인 필요 + ) + model_inputs = tokenizer([text], return_tensors="pt").to(model.device) + + # 스트리머 설정 + streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True) + + # 생성 인자 설정 + generation_kwargs = dict( + **model_inputs, + streamer=streamer, + max_new_tokens=150, + do_sample=True, + temperature=0.3, + top_p=0.9, + pad_token_id=tokenizer.eos_token_id + ) + + # 별도 스레드에서 생성 실행 + thread = Thread(target=model.generate, kwargs=generation_kwargs) + thread.start() + + # 제너레이터 함수 정의 + def generate(): + for new_text in streamer: + if new_text: + print(f'new Text: {new_text}') + yield json.dumps({"kind": "text", "text": new_text}) + "\n" + print(f'End time: {datetime.now()}') + + return generate + + +app = FastAPI() + +# CORS 설정 추가 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 모든 출처 허용 + allow_credentials=True, + allow_methods=["*"], # 모든 HTTP 메서드 허용 + allow_headers=["*"], # 모든 헤더 허용 +) + +# === 1. 기초 데이터 주입 API === +class Item(BaseModel): + context: list + sessionId: str + +@app.post("/set-data") +async def set_data(query: Item): + """ + 클라이언트로부터 받은 인사 데이터를 자연어 문장으로 변환하여 벡터 DB에 저장합니다. + 기존 세션 데이터는 삭제 후 재생성됩니다. + """ + # 기존 데이터 삭제 + collection.delete( + where={"sessionId": query.sessionId} + ) + + # 삭제 확인 (디버깅용) + remaining_count = collection.get(where={"sessionId": query.sessionId}) + print(f"남은 데이터 수: {len(remaining_count['ids'])}") + + doc_list = [] + for q in query.context: + # 각 필드를 안전하게 추출 (None 방어) + name = q.get('name') or "" + dept = q.get('deptNm') or "" + grade = q.get('gradeNm') or "" + position = q.get('ptsnNm') or "" + office_phone = q.get('ofcePhn') or "" + mobile_phone = q.get('mblPhn') or "" + chief_name = q.get('chiefNm') or "" + state_code = q.get('state') or "" + + # 상태 코드 한글화 + state_map = {'C': '재직', 'T': '퇴사', 'H': '휴직'} + status = state_map.get(state_code, "정보없음") + + # [핵심] 검색 엔진이 좋아할만한 서술형 문장 생성 + # 부서명과 이름을 앞부분에 배치하여 가중치 유도 + if name == '': + doc = ( + f"부서: {dept}. " + f"해당 {dept}의 부서장은 {chief_name}입니다." + ) + else: + doc = ( + f"부서: {dept}. 이름: {name}. {dept} 소속의 {name} {grade}입니다. " + f"직위는 {position}이며 현재 {status} 중입니다. " + f"사내 전화번호(사선)는 {office_phone}입니다." + ) + doc_list.append(doc) + + init(query.sessionId, doc_list) + + return {"status": "success", "message": f"{len(query.context)}건의 데이터가 로드되었습니다."} + + +@app.get("/") +def question(sessionId: str, query: str): + """ + 질의응답 API 엔드포인트 + """ + generate = query_and_summarize_stream(sessionId=sessionId, query=query) + return StreamingResponse(generate(), media_type="application/x-ndjson") + + +# 개발용 실행 (직접 실행 시) +if __name__ == "__main__": + import uvicorn + print("서버 시작: uvicorn manual:app --reload") + # uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/org_transformer_offline.py b/org_transformer_offline.py new file mode 100644 index 0000000..74f4255 --- /dev/null +++ b/org_transformer_offline.py @@ -0,0 +1,57 @@ +import json +from typing import List, Union + +import chromadb +from chromadb.utils import embedding_functions + +# === 경로 설정 (모두 로컬) === +EMBEDDING_MODEL_PATH = "./models/ko-sroberta-multitask" + +# 2. 벡터 DB 설정 +persist_directory = "./chroma_db" +chroma_client = chromadb.PersistentClient(path=persist_directory) + +# 임베딩 함수 설정 +embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction( + model_name=EMBEDDING_MODEL_PATH, # 로컬 폴더 경로 가능 + device="cpu", + normalize_embeddings=True +) + +# 컬렉션 생성 또는 가져오기 +collection = chroma_client.get_or_create_collection( + name="orgchart", + embedding_function=embedding_fn, + metadata={"hnsw:space": "cosine"} +) + + +def init(sessionId: str, data: List[Union[str, dict]]): + """ + 데이터를 벡터 DB에 초기화(저장)합니다. + + Args: + sessionId (str): 세션 ID + data (List[Union[str, dict]]): 저장할 데이터 리스트 (문자열 또는 딕셔너리) + """ + print(f'{sessionId} init start') + + # 문서 ID 생성 + doc_ids = [f"{sessionId}_{i}" for i in range(len(data))] + + # 데이터 처리: 문자열이면 그대로, 객체면 JSON 문자열로 변환 + documents = [] + for item in data: + if isinstance(item, str): + documents.append(item) + else: + documents.append(json.dumps(item, ensure_ascii=False)) + + # 벡터 DB에 추가 + collection.add( + documents=documents, + ids=doc_ids, + metadatas=[{"sessionId": sessionId} for _ in doc_ids] + ) + + print(f'{sessionId} init end') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..1ef6a41262ad33c324be938082ef3b9937305562 GIT binary patch literal 4184 zcmb`KOK%%T5QOI(AU}nINJ_GO$RXDt1`OnslOR4sQjaCGOInhDe3JUQwmC~#4j>>{ zkg_@bsHv{*nf>SQn=;q4ED!p$<)-ZQ_ZL0ma;6X8=jG?}lU5o%Tdmt>QATB<|Ifqv zJnYWO^oO?l@=|t+-5u>eoWy=98$~1Y#dn$a%G&mxlX9tbY>aCrJo9od-&;NSPqfBo z99U*rZ6o(SR`E7kb~P&p<$cgja?jIaSUlYI7Epc$vv^3$)IMpT1A7@j+(>2D`VC{HR+XjtCcQvXPpvfSQ{f- zDH}T{^GLA|ioOo~=;%To$->am`{?8`m-nL{c0N)zDu!l^3Z^~`_PEmePClu3ucIoX zS<7}jU~jl`tN71CX)WUkw)CS}fB* zjq+UU7dW6NP=bBXew`_AdBIAjNN5$@wV&zMNo4yi_I9V3lL3xfmFiCIqHuF_AH356jeMEiww*>!^kHt9 zAFXBt6Z=WFBSmHkWuJPUruFCE`k?hc^ly4BtK#~yi$_I+$%k0uck9t>k78gNE9NI- zQ%lffhv8EvmUra}lhcTt0!7pIdXkxirj{=XbK zf8HCv&b9Y?c2bJZr}uEiNr_TitGSqBvNh#vUz=fXpuS5@x!JK6?Xk;1YZZ_^)k2h3 z4^t<*hUrl!$WFOAgKm!Vj!5ofR|6$H=%J^~iKlTYf{JtEt@U>m7W9I9jV`H-`Ya;` zlXDt)Ge;M?CKKzP)Yv$}i|}<~rJfi>u;S^ITxDi%qhnFDrv$8dHf1bsQ}BMJ-@T*P z7EIi%2o^pY-HRDzXmcG^%kv1kiyW{xKnDshF<&b4D+`lMN&)Pk741D!>8Q|qkS zOJ8^U((O!?uSZt#b(PrPpXJx`x%?ihK0C9*AoU#we@+KZ-sE9A&?}ZJeLOXXHyw8! zd&1@~?EwW0`JM(pp0clPm{s4;R`Z!kr;j-EtQl*nuIG4SjRkH9Z>l^w2VKz{|MrIM_k?p87D&y@vM=^xg>v z**OUk14^;7(swUrU*E1ihF+}_+i^2sYPgcT6+9l1R`qpScO{Lo&Lz5?I|8?adKa(f zobTA2%P&eln7WFK3jyjplIP(uqT{YJwIj1vF3SGpU<2{1j^OORObg>f zcO)wBTUp-4DwmZBBHHI#yL=AL@f@B|;K+9>F#G1@yNag|9R+XhiGyYZJ%#G5WHdj# z3p|PYeXx~pO?BG2p5%EFqM5MxeF{Zzj*{0}cgN-&d)p)9qE1kFZhcdXogN~9+czkq F{}(>GZe0KX literal 0 HcmV?d00001 diff --git a/test.py b/test.py new file mode 100644 index 0000000..69961df --- /dev/null +++ b/test.py @@ -0,0 +1,5 @@ +# 테스트용 스크립트 +text = '산재예방부 부서장은 누구야?' + +# '부'라는 글자가 포함되어 있는지 확인 +print(text.find('부')) diff --git a/vector_transformer.py b/vector_transformer.py new file mode 100644 index 0000000..a3e6889 --- /dev/null +++ b/vector_transformer.py @@ -0,0 +1,55 @@ +import chromadb +from chromadb.utils import embedding_functions + +# 2. 벡터 DB 설정 +persist_directory = "./chroma_db" +chroma_client = chromadb.PersistentClient(path=persist_directory) + +# ✅ Chroma 전용 임베딩 함수 사용 (오류 방지) +sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction( + model_name="all-MiniLM-L6-v2" +) + +# 컬렉션 생성 +collection = chroma_client.get_or_create_collection( + name="manuals", + embedding_function=sentence_transformer_ef # ← 여기가 핵심! +) + + +def init(job: str): + """ + 직무별 매뉴얼 데이터를 벡터 DB에 초기화합니다. + + Args: + job (str): 직무 코드 (예: 'FI', 'HR') + """ + print(f'{job} init start') + # 1. 문서 준비 (실제로는 PDF/Word 등에서 추출) + manuals = [ + "지출 결의서는 사용 목적, 금액, 일자, 증빙 서류를 반드시 첨부하여 전자 결재 시스템에 등록해야 합니다.", + "월말 마감은 매월 25일부터 시작되며, 모든 부서는 28일까지 비용 집행 내역을 최종 확정해야 합니다.", + "외화 송금은 반드시 외환관리부의 사전 승인을 받은 후 금융팀을 통해 진행되어야 하며, 계약서 사본을 첨부해야 합니다.", + "세금계산서는 발행일로부터 10일 이내에 ERP에 등록되지 않으면 비용 처리가 불가합니다.", + "장기자산(차량, 사무기기 등)은 매년 1월에 정기 감가상각 점검을 받아야 하며, 자산관리부서가 이를 주관합니다.", + "현금 보관은 원칙적으로 금지되며, 불가피한 경우는 금고 보관 후 당일 중 재무팀에 입금 처리해야 합니다.", + "연말 정산 대상 직원은 매년 12월 10일까지 개인 소득공제 자료를 인사 시스템에 제출해야 합니다.", + "예산 초과 지출은 사전에 재무부와 협의 후 예산 조정 승인을 받아야 하며, 미승인 시 결재가 거부됩니다.", + "재무 제표 초안은 분기 마감 후 5영업일 이내에 감사법인에 제출되어야 하며, 최종 승인은 CFO가 담당합니다.", + ] + + # 문서 ID 생성 및 추가 + doc_ids = [f"DOC_{job}_{i}" for i in range(len(manuals))] + + collection.add( + documents=manuals, + ids=doc_ids, + metadatas=[{"source": "fi_manual_v1.pdf", "version": 1.0, "dept": job} for _ in doc_ids] + ) + + print(f'{job} init end') + + +if __name__ == "__main__": + # FI : 재무 HR : 인사 + init(job="FI") diff --git a/vector_transformer_offline.py b/vector_transformer_offline.py new file mode 100644 index 0000000..138d9a3 --- /dev/null +++ b/vector_transformer_offline.py @@ -0,0 +1,59 @@ +import chromadb +from chromadb.utils import embedding_functions + +# === 경로 설정 (모두 로컬) === +EMBEDDING_MODEL_PATH = "./models/all-MiniLM-L6-v2" + +# 2. 벡터 DB 설정 +persist_directory = "./chroma_db" +chroma_client = chromadb.PersistentClient(path=persist_directory) + +# 임베딩 함수 설정 +embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction( + model_name=EMBEDDING_MODEL_PATH, # 로컬 폴더 경로 가능 + device="cpu" +) + +# 컬렉션 생성 또는 가져오기 +collection = chroma_client.get_or_create_collection( + name="manuals", + embedding_function=embedding_fn +) + + +def init(job: str): + """ + 오프라인 환경에서 직무별 매뉴얼 데이터를 벡터 DB에 초기화합니다. + + Args: + job (str): 직무 코드 (예: 'FI', 'HR') + """ + print(f'{job} init start') + # 1. 문서 준비 (실제로는 PDF/Word 등에서 추출) + manuals = [ + "지출 결의서는 사용 목적, 금액, 일자, 증빙 서류를 반드시 첨부하여 전자 결재 시스템에 등록해야 합니다.", + "월말 마감은 매월 25일부터 시작되며, 모든 부서는 28일까지 비용 집행 내역을 최종 확정해야 합니다.", + "외화 송금은 반드시 외환관리부의 사전 승인을 받은 후 금융팀을 통해 진행되어야 하며, 계약서 사본을 첨부해야 합니다.", + "세금계산서는 발행일로부터 10일 이내에 ERP에 등록되지 않으면 비용 처리가 불가합니다.", + "장기자산(차량, 사무기기 등)은 매년 1월에 정기 감가상각 점검을 받아야 하며, 자산관리부서가 이를 주관합니다.", + "현금 보관은 원칙적으로 금지되며, 불가피한 경우는 금고 보관 후 당일 중 재무팀에 입금 처리해야 합니다.", + "연말 정산 대상 직원은 매년 12월 10일까지 개인 소득공제 자료를 인사 시스템에 제출해야 합니다.", + "예산 초과 지출은 사전에 재무부와 협의 후 예산 조정 승인을 받아야 하며, 미승인 시 결재가 거부됩니다.", + "재무 제표 초안은 분기 마감 후 5영업일 이내에 감사법인에 제출되어야 하며, 최종 승인은 CFO가 담당합니다.", + ] + + # 문서 ID 생성 및 추가 + doc_ids = [f"DOC_{job}_{i}" for i in range(len(manuals))] + + collection.add( + documents=manuals, + ids=doc_ids, + metadatas=[{"source": "fi_manual_v1.pdf", "version": 1.0, "dept": job} for _ in doc_ids] + ) + + print(f'{job} init end') + + +if __name__ == "__main__": + # FI : 재무 HR : 인사 + init(job="FI")