first
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.venv
|
||||||
|
.idea
|
||||||
|
chroma_db
|
||||||
|
models
|
||||||
|
*.iml
|
||||||
31
README.md
Normal file
31
README.md
Normal file
@@ -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
|
||||||
|
|
||||||
|
|
||||||
BIN
__pycache__/org_offline.cpython-312.pyc
Normal file
BIN
__pycache__/org_offline.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/org_transformer_offline.cpython-312.pyc
Normal file
BIN
__pycache__/org_transformer_offline.cpython-312.pyc
Normal file
Binary file not shown.
6
download_embed.py
Normal file
6
download_embed.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from sentence_transformers import SentenceTransformer
|
||||||
|
|
||||||
|
# 모델 다운로드 및 로컬 저장
|
||||||
|
# 이 스크립트는 인터넷 연결이 필요하며, 한 번 실행 후에는 오프라인에서 모델을 사용할 수 있게 해줍니다.
|
||||||
|
model = SentenceTransformer('jhgan/ko-sroberta-multitask')
|
||||||
|
model.save('./models/ko-sroberta-multitask')
|
||||||
127
org.html
Normal file
127
org.html
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
width:100%;
|
||||||
|
height:100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-wrap {
|
||||||
|
padding: 0 20%;
|
||||||
|
}
|
||||||
|
.chat {
|
||||||
|
height: 95vh;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
white-space: pre-wrap; /* 줄바꿈 유지 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt {
|
||||||
|
width:100%;
|
||||||
|
height:3vh;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-msg {
|
||||||
|
color: blue;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-msg {
|
||||||
|
color: black;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="chat-wrap">
|
||||||
|
<div class="chat"></div>
|
||||||
|
<input type="text" class="prompt" value="재난관리부 부서장은 누구야?" placeholder="질문을 입력하고 Enter를 누르세요">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const promptInput = document.querySelector('.prompt');
|
||||||
|
const chatDiv = document.querySelector('.chat');
|
||||||
|
|
||||||
|
promptInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const query = promptInput.value;
|
||||||
|
if (!query.trim()) return;
|
||||||
|
|
||||||
|
promptInput.value = '';
|
||||||
|
|
||||||
|
// 사용자 질문 표시
|
||||||
|
const userDiv = document.createElement('div');
|
||||||
|
userDiv.className = 'user-msg';
|
||||||
|
userDiv.textContent = `Q: ${query}`;
|
||||||
|
chatDiv.appendChild(userDiv);
|
||||||
|
|
||||||
|
// 봇 답변 영역 생성
|
||||||
|
const botDiv = document.createElement('div');
|
||||||
|
botDiv.className = 'bot-msg';
|
||||||
|
botDiv.textContent = 'A: ';
|
||||||
|
chatDiv.appendChild(botDiv);
|
||||||
|
|
||||||
|
// 스크롤 하단으로 이동
|
||||||
|
chatDiv.scrollTop = chatDiv.scrollHeight;
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.open('GET', `http://127.0.0.1:8000/?sessionId=20033011&query=${encodeURIComponent(query)}`);
|
||||||
|
|
||||||
|
let seenBytes = 0;
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
xhr.onprogress = function () {
|
||||||
|
const newData = xhr.responseText.substring(seenBytes);
|
||||||
|
console.log(newData)
|
||||||
|
seenBytes = xhr.responseText.length;
|
||||||
|
|
||||||
|
buffer += newData;
|
||||||
|
|
||||||
|
// 줄바꿈 기준으로 분리
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
|
||||||
|
// 마지막 부분은 불완전할 수 있으므로 버퍼에 남김
|
||||||
|
buffer = lines.pop();
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(line);
|
||||||
|
if (json.kind === 'text') {
|
||||||
|
botDiv.textContent += json.text;
|
||||||
|
chatDiv.scrollTop = chatDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("JSON parsing error:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onloadend = function() {
|
||||||
|
// 남은 버퍼 처리 (혹시 마지막에 줄바꿈이 없었을 경우)
|
||||||
|
if (buffer.trim()) {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(buffer);
|
||||||
|
if (json.kind === 'text') {
|
||||||
|
botDiv.textContent += json.text;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Final JSON parsing error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
297
org_offline.py
Normal file
297
org_offline.py
Normal file
@@ -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)
|
||||||
57
org_transformer_offline.py
Normal file
57
org_transformer_offline.py
Normal file
@@ -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')
|
||||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
5
test.py
Normal file
5
test.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 테스트용 스크립트
|
||||||
|
text = '산재예방부 부서장은 누구야?'
|
||||||
|
|
||||||
|
# '부'라는 글자가 포함되어 있는지 확인
|
||||||
|
print(text.find('부'))
|
||||||
55
vector_transformer.py
Normal file
55
vector_transformer.py
Normal file
@@ -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")
|
||||||
59
vector_transformer_offline.py
Normal file
59
vector_transformer_offline.py
Normal file
@@ -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")
|
||||||
Reference in New Issue
Block a user