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
|
||||
BIN
__pycache__/manual.cpython-312.pyc
Normal file
BIN
__pycache__/manual.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/offline_manual.cpython-312.pyc
Normal file
BIN
__pycache__/offline_manual.cpython-312.pyc
Normal file
Binary file not shown.
16
download_embed.py
Normal file
16
download_embed.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from sentence_transformers import SentenceTransformer
|
||||
model = SentenceTransformer('all-MiniLM-L6-v2')
|
||||
model.save('./all-MiniLM-L6-v2')
|
||||
|
||||
# # HF CLI 설치 (처음 한 번만)
|
||||
# pip install huggingface_hub
|
||||
#
|
||||
# # 로그인 (오프라인 사용 전 한 번만 필요)
|
||||
# huggingface-cli login # 토큰 입력 (https://huggingface.co/settings/tokens)
|
||||
#
|
||||
# # 모델 로컬 저장
|
||||
# huggingface-cli download Qwen/Qwen3-0.6B --local-dir ./Qwen3-0.6B --local-dir-use-symlinks False
|
||||
#
|
||||
#
|
||||
#
|
||||
# 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
|
||||
147
manual.py
Normal file
147
manual.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import os
|
||||
|
||||
import torch
|
||||
from sentence_transformers import SentenceTransformer
|
||||
import chromadb
|
||||
from chromadb.utils import embedding_functions
|
||||
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
|
||||
from fastapi import FastAPI
|
||||
|
||||
# 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 # ← 여기가 핵심!
|
||||
)
|
||||
|
||||
_model = None
|
||||
_tokenizer = None
|
||||
|
||||
def get_qwen_model() :
|
||||
global _model, _tokenizer
|
||||
if _model is None:
|
||||
model_name = "Qwen/Qwen3-0.6B"
|
||||
_tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
|
||||
_model = AutoModelForCausalLM.from_pretrained(
|
||||
model_name,
|
||||
torch_dtype=torch.float32, # CPU 안정성
|
||||
device_map="auto",
|
||||
trust_remote_code=True
|
||||
)
|
||||
return _model, _tokenizer
|
||||
|
||||
def init(job: str) :
|
||||
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)
|
||||
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')
|
||||
|
||||
# 3. 질의 처리
|
||||
def query_and_summarize(job: str, query: str, top_k: int = 3):
|
||||
|
||||
# 관련 문서 검색
|
||||
results = collection.query(
|
||||
query_texts=[query],
|
||||
n_results=5,
|
||||
where={"dept": job}
|
||||
)
|
||||
# results = collection.query(query_texts=[query], n_results=top_k)
|
||||
cosine_similarities = [1 - d for d in results['distances'][0]]
|
||||
print("유사도:", cosine_similarities)
|
||||
# 출력 예: [0.610, 0.473, 0.154, 0.142]
|
||||
|
||||
context_with_score = ""
|
||||
for i, (doc, dist) in enumerate(zip(results['documents'][0], results['distances'][0])):
|
||||
sim = 1 - dist
|
||||
context_with_score += f"[문서 {i+1} | 유사도: {sim:.3f}]\n{doc}\n\n"
|
||||
|
||||
print(context_with_score)
|
||||
print("\n\n\n\n\n")
|
||||
top_doc = results['documents'][0][0]
|
||||
|
||||
# ✅ 명시적으로 모델과 토크나이저 로드
|
||||
model, tokenizer = get_qwen_model()
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "당신은 회사 재무/회계 업무 전문 어시스턴트입니다. 문서 내용은 그대로 사용자에게 보여 줘야 하며 이를 기반으로 부가설명을 정확하고 상세하게 답변하세요."},
|
||||
{"role": "user", "content": f"다음 문서를 참고하세요:\n{top_doc}\n\n질문: {query}"}
|
||||
]
|
||||
|
||||
text = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
enable_thinking=False # Switches between thinking and non-thinking modes. Default is True.
|
||||
)
|
||||
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
|
||||
|
||||
# conduct text completion
|
||||
generated_ids = model.generate(
|
||||
**model_inputs,
|
||||
max_new_tokens=500
|
||||
)
|
||||
output_ids = generated_ids[0][len(model_inputs.input_ids[0]):].tolist()
|
||||
|
||||
# parsing thinking content
|
||||
try:
|
||||
# rindex finding 151668 (</think>)
|
||||
index = len(output_ids) - output_ids[::-1].index(151668)
|
||||
except ValueError:
|
||||
index = 0
|
||||
|
||||
thinking_content = tokenizer.decode(output_ids[:index], skip_special_tokens=True).strip("\n")
|
||||
end_think_id = tokenizer.convert_tokens_to_ids("</think>")
|
||||
if end_think_id in output_ids:
|
||||
idx = len(output_ids) - output_ids[::-1].index(end_think_id)
|
||||
else:
|
||||
idx = 0
|
||||
content = tokenizer.decode(output_ids[idx:], skip_special_tokens=True).strip()
|
||||
|
||||
print(top_doc)
|
||||
print("\n\n\n\n\n")
|
||||
print("thinking content:", thinking_content)
|
||||
print("\n\n\n\n\n")
|
||||
return content
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/")
|
||||
def question(query: str) :
|
||||
user_query = query
|
||||
answer = query_and_summarize(job="FI", query=user_query)
|
||||
return {"answer": answer}
|
||||
|
||||
# 예시 사용
|
||||
if __name__ == "__main__":
|
||||
# init(job="FI")
|
||||
# FI : 재무 HR : 인사
|
||||
print('1')
|
||||
# 실행방법 uvicorn manual:app --reload
|
||||
159
offline_manual.py
Normal file
159
offline_manual.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import os
|
||||
|
||||
import torch
|
||||
from sentence_transformers import SentenceTransformer
|
||||
import chromadb
|
||||
from chromadb.utils import embedding_functions
|
||||
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
|
||||
from fastapi import FastAPI
|
||||
|
||||
# # === 경로 설정 (모두 로컬) ===
|
||||
QWEN_MODEL_PATH = "./models/Qwen3-0.6B"
|
||||
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
|
||||
)
|
||||
|
||||
_model = None
|
||||
_tokenizer = None
|
||||
|
||||
def get_qwen_model() :
|
||||
global _model, _tokenizer
|
||||
if _model is None:
|
||||
model_name = "Qwen/Qwen3-0.6B"
|
||||
_tokenizer = AutoTokenizer.from_pretrained(
|
||||
QWEN_MODEL_PATH,
|
||||
trust_remote_code=True,
|
||||
local_files_only=True # 🔒 오프라인 강제
|
||||
)
|
||||
_model = AutoModelForCausalLM.from_pretrained(
|
||||
QWEN_MODEL_PATH,
|
||||
torch_dtype=torch.float32, # CPU 안정성
|
||||
device_map="auto",
|
||||
trust_remote_code=True,
|
||||
local_files_only=True # 🔒 오프라인 강제
|
||||
)
|
||||
return _model, _tokenizer
|
||||
|
||||
def init(job: str) :
|
||||
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)
|
||||
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')
|
||||
|
||||
# 3. 질의 처리
|
||||
def query_and_summarize(job: str, query: str, top_k: int = 3):
|
||||
|
||||
# 관련 문서 검색
|
||||
results = collection.query(
|
||||
query_texts=[query],
|
||||
n_results=5,
|
||||
where={"dept": job}
|
||||
)
|
||||
# results = collection.query(query_texts=[query], n_results=top_k)
|
||||
cosine_similarities = [1 - d for d in results['distances'][0]]
|
||||
print("유사도:", cosine_similarities)
|
||||
# 출력 예: [0.610, 0.473, 0.154, 0.142]
|
||||
|
||||
context_with_score = ""
|
||||
for i, (doc, dist) in enumerate(zip(results['documents'][0], results['distances'][0])):
|
||||
sim = 1 - dist
|
||||
context_with_score += f"[문서 {i+1} | 유사도: {sim:.3f}]\n{doc}\n\n"
|
||||
|
||||
print(context_with_score)
|
||||
print("\n\n\n\n\n")
|
||||
top_doc = results['documents'][0][0]
|
||||
|
||||
# ✅ 명시적으로 모델과 토크나이저 로드
|
||||
model, tokenizer = get_qwen_model()
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "당신은 회사 재무/회계 업무 전문 어시스턴트입니다. 문서 내용은 그대로 사용자에게 보여 줘야 하며 이를 기반으로 부가설명을 정확하고 상세하게 답변하세요."},
|
||||
{"role": "user", "content": f"다음 문서를 참고하세요:\n{top_doc}\n\n질문: {query}"}
|
||||
]
|
||||
|
||||
text = tokenizer.apply_chat_template(
|
||||
messages,
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
enable_thinking=True # Switches between thinking and non-thinking modes. Default is True.
|
||||
)
|
||||
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
|
||||
|
||||
# conduct text completion
|
||||
generated_ids = model.generate(
|
||||
**model_inputs,
|
||||
max_new_tokens=500
|
||||
)
|
||||
output_ids = generated_ids[0][len(model_inputs.input_ids[0]):].tolist()
|
||||
|
||||
# parsing thinking content
|
||||
try:
|
||||
# rindex finding 151668 (</think>)
|
||||
index = len(output_ids) - output_ids[::-1].index(151668)
|
||||
except ValueError:
|
||||
index = 0
|
||||
|
||||
thinking_content = tokenizer.decode(output_ids[:index], skip_special_tokens=True).strip("\n")
|
||||
end_think_id = tokenizer.convert_tokens_to_ids("</think>")
|
||||
if end_think_id in output_ids:
|
||||
idx = len(output_ids) - output_ids[::-1].index(end_think_id)
|
||||
else:
|
||||
idx = 0
|
||||
content = tokenizer.decode(output_ids[idx:], skip_special_tokens=True).strip()
|
||||
|
||||
print(top_doc)
|
||||
print("\n\n\n\n\n")
|
||||
print("thinking content:", thinking_content)
|
||||
print("\n\n\n\n\n")
|
||||
return content
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/")
|
||||
def question(query: str) :
|
||||
print(1)
|
||||
answer = query_and_summarize(job="FI", query=query)
|
||||
return {"answer": answer}
|
||||
|
||||
# 예시 사용
|
||||
if __name__ == "__main__":
|
||||
# init(job="FI")
|
||||
# FI : 재무 HR : 인사
|
||||
print(1)
|
||||
user_query = "외화 송금 방법?"
|
||||
# answer = query_and_summarize(job="FI", query=user_query)
|
||||
# print(answer)
|
||||
# 실행방법 uvicorn manual:app --reload
|
||||
BIN
requirements.txx
Normal file
BIN
requirements.txx
Normal file
Binary file not shown.
Reference in New Issue
Block a user