Spaces:
Runtime error
Runtime error
Upload 2 files
Browse files- llm_service.py +123 -0
- vector_service.py +56 -0
llm_service.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import List, Dict, Optional
|
| 3 |
+
from openai import OpenAI
|
| 4 |
+
|
| 5 |
+
USE_MOCK = os.getenv("USE_MOCK_LLM", "0") == "1"
|
| 6 |
+
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
| 7 |
+
|
| 8 |
+
_client = None
|
| 9 |
+
# Lazy initialization of OpenAI client
|
| 10 |
+
def _client_lazy():
|
| 11 |
+
global _client
|
| 12 |
+
if _client is None:
|
| 13 |
+
_client = OpenAI() # ํ๊ฒฝ๋ณ์ OPENAI_API_KEY ํ์
|
| 14 |
+
return _client
|
| 15 |
+
|
| 16 |
+
# Mock functions for testing without actual LLM calls
|
| 17 |
+
def _mock_questions(user_message: str, user_profile: Dict, restrict_topics: Optional[List[str]] = None) -> List[str]:
|
| 18 |
+
# ์ฌ์ฉ์ ๋ฉ์์ง ๊ธฐ๋ฐ์ผ๋ก ๋ค์ํ mock ์ง๋ฌธ ์์ฑ
|
| 19 |
+
if "๋์ถ" in user_message:
|
| 20 |
+
return ["๋์ถ ๊ธ๋ฆฌ๋ ์ด๋ป๊ฒ ๊ฒฐ์ ๋๋์?", "์ ์ฉ๋์ถ๊ณผ ๋ด๋ณด๋์ถ์ ์ฐจ์ด๋?", "๋์ถ ํ๋๋ ์ด๋ป๊ฒ ์ฐ์ ๋๋์?"]
|
| 21 |
+
elif "ํ๋" in user_message or "ํฌ์" in user_message:
|
| 22 |
+
return ["ํ๋์ ์ฃผ์ ํฌ์์ ์ฐจ์ด๋?", "์์ ์ ์ธ ํ๋ ์ํ ์ถ์ฒํด์ฃผ์ธ์", "ํ๋ ์์ต๋ฅ ์ ์ด๋ป๊ฒ ํ์ธํ๋์?"]
|
| 23 |
+
else:
|
| 24 |
+
return [
|
| 25 |
+
"์๊ธ๊ณผ ์ ๊ธ์ ์ฐจ์ด๊ฐ ๋ญ์์?",
|
| 26 |
+
"๋จ๊ธฐ ์ ์ถ์ ๋ ์ ๋ฆฌํ ์ํ์ ๋ฌด์์ธ๊ฐ์?",
|
| 27 |
+
"์ ๊ธ์ด ์๊ธ๋ณด๋ค ์ด์๊ฐ ํญ์ ๋์๊ฐ์?",
|
| 28 |
+
"์๊ธ/์ ๊ธ ์ค๋ํด์ง ์ ๋ถ์ด์ต์ด ์๋์?",
|
| 29 |
+
"๊ธ๋ฆฌ ๋น๊ต ์ ์ด๋ค ๊ธฐ์ค์ ๋ด์ผ ํ๋์?"
|
| 30 |
+
]
|
| 31 |
+
return base if 'base' in locals() else []
|
| 32 |
+
|
| 33 |
+
def _mock_answer(question: str, context: str) -> str:
|
| 34 |
+
return f"๋ชจ์์๋ต: ์ง๋ฌธ \"{question}\"์ ๋ํด ๋ฌธ์ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์์ฝํ๋ฉด โ {context[:120]}..."
|
| 35 |
+
|
| 36 |
+
def generate_questions_from_context(user_message: str, user_profile: Dict, similar_docs: List[Dict]) -> List[str]:
|
| 37 |
+
"""๋ฒกํฐ DB์์ ๊ฒ์๋ ๋ฌธ์ ๋ด์ฉ์ ๊ธฐ๋ฐ์ผ๋ก ์ง๋ฌธ ์์ฑ"""
|
| 38 |
+
if USE_MOCK:
|
| 39 |
+
return _mock_questions(user_message, user_profile, None)
|
| 40 |
+
|
| 41 |
+
# ๋ฌธ์ ๋ด์ฉ ์ถ์ถ ๋ฐ ํฌ๋งทํ
|
| 42 |
+
context_parts = []
|
| 43 |
+
for i, doc in enumerate(similar_docs[:5], 1):
|
| 44 |
+
doc_content = doc.get('content', '') or doc.get('text', '')
|
| 45 |
+
doc_meta = doc.get('meta', {})
|
| 46 |
+
context_parts.append(f"[๋ฌธ์ {i}] {doc_meta}\n๋ด์ฉ: {doc_content[:200]}...")
|
| 47 |
+
|
| 48 |
+
context = "\n\n".join(context_parts)
|
| 49 |
+
|
| 50 |
+
prompt = f"""
|
| 51 |
+
๋น์ ์ ๊ธ์ต ์ฑ๋ด์ '์ง๋ฌธ ์ถ์ฒ' ์ญํ ์
๋๋ค.
|
| 52 |
+
์ฌ์ฉ์์ ์ง๋ฌธ๊ณผ ๊ด๋ จ๋ ๋ฌธ์ ๋ด์ฉ์ ์ฐธ๊ณ ํ์ฌ, ์ฌ์ฉ์๊ฐ ์ถ๊ฐ๋ก ๊ถ๊ธํดํ ๋งํ ์ง๋ฌธ 5๊ฐ๋ฅผ ์ ์ํ์ธ์.
|
| 53 |
+
|
| 54 |
+
**์ค์**: ์๋ ๊ฒ์๋ ๋ฌธ์ ๋ด์ฉ์ ๊ธฐ๋ฐํ์ฌ ๋ต๋ณ ๊ฐ๋ฅํ ์ง๋ฌธ๋ง ์์ฑํ์ธ์.
|
| 55 |
+
์ง๋ฌธ์ ์งง๊ณ ํด๋ฆญํ๊ธฐ ์ฝ๊ฒ ๋ง๋์ธ์.
|
| 56 |
+
|
| 57 |
+
์ฌ์ฉ์ ์ง๋ฌธ: "{user_message}"
|
| 58 |
+
์ฌ์ฉ์ ํ๋กํ: {user_profile}
|
| 59 |
+
|
| 60 |
+
[๊ฒ์๋ ๊ด๋ จ ๋ฌธ์]
|
| 61 |
+
{context}
|
| 62 |
+
|
| 63 |
+
์ถ๋ ฅ์ ๊ฐ ์ง๋ฌธ์ ํ ์ค์ฉ ๋์ด๋ง ํ์ธ์.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
client = _client_lazy()
|
| 67 |
+
resp = client.chat.completions.create(
|
| 68 |
+
model=OPENAI_MODEL,
|
| 69 |
+
messages=[{"role": "user", "content": prompt}],
|
| 70 |
+
temperature=0.3,
|
| 71 |
+
)
|
| 72 |
+
content = resp.choices[0].message.content.strip()
|
| 73 |
+
lines = [l.strip("- ").strip() for l in content.split("\n") if l.strip()]
|
| 74 |
+
return lines[:5]
|
| 75 |
+
|
| 76 |
+
def generate_questions(user_message: str, user_profile: Dict, restrict_topics: Optional[List[str]] = None) -> List[str]:
|
| 77 |
+
if USE_MOCK:
|
| 78 |
+
return _mock_questions(user_message, user_profile, restrict_topics)
|
| 79 |
+
|
| 80 |
+
topics_line = f"\n์ฐธ๊ณ ์ฃผ์ : {', '.join(restrict_topics)}" if restrict_topics else ""
|
| 81 |
+
prompt = f"""
|
| 82 |
+
๋น์ ์ ๊ธ์ต ์ฑ๋ด์ '์ง๋ฌธ ์ถ์ฒ' ์ญํ ์
๋๋ค.
|
| 83 |
+
์ฌ์ฉ์ ์
๋ ฅ๊ณผ ํ๋กํ์ ์ฐธ๊ณ ํ์ฌ ๊ธ์ต ๊ด๋ จ(์๊ธ/์ ๊ธ, ๋์ถ, ํ๋, ๋ณดํ, ์นด๋ ๋ฑ) ํ์ ์ง๋ฌธ 5๊ฐ๋ฅผ ์ ์ํ์ธ์.
|
| 84 |
+
์ฌ์ฉ์์ ์ง๋ฌธ ์๋๋ฅผ ์ ํํ ํ์
ํ์ฌ ๊ด๋ จ์ฑ ๋์ ์ง๋ฌธ์ ์์ฑํ์ธ์.
|
| 85 |
+
์ง๋ฌธ์ ์งง๊ณ ํด๋ฆญํ๊ธฐ ์ฝ๊ฒ ๋ง๋์ธ์.
|
| 86 |
+
์ฌ์ฉ์ ์
๋ ฅ: "{user_message}"
|
| 87 |
+
์ฌ์ฉ์ ํ๋กํ: {user_profile}{topics_line}
|
| 88 |
+
์ถ๋ ฅ์ ๊ฐ ์ง๋ฌธ์ ํ ์ค์ฉ ๋์ด๋ง ํ์ธ์.
|
| 89 |
+
"""
|
| 90 |
+
client = _client_lazy()
|
| 91 |
+
resp = client.chat.completions.create(
|
| 92 |
+
model=OPENAI_MODEL,
|
| 93 |
+
messages=[{"role": "user", "content": prompt}],
|
| 94 |
+
temperature=0.3,
|
| 95 |
+
)
|
| 96 |
+
content = resp.choices[0].message.content.strip()
|
| 97 |
+
lines = [l.strip("- ").strip() for l in content.split("\n") if l.strip()]
|
| 98 |
+
return lines[:5]
|
| 99 |
+
|
| 100 |
+
def generate_answer(question: str, context: str, user_profile: Optional[Dict] = None) -> str:
|
| 101 |
+
if USE_MOCK:
|
| 102 |
+
return _mock_answer(question, context)
|
| 103 |
+
|
| 104 |
+
profile_text = f"\n[์ฌ์ฉ์ ํ๋กํ]\n{user_profile}\n" if user_profile else ""
|
| 105 |
+
|
| 106 |
+
prompt = f"""
|
| 107 |
+
๋ค์ ๋ฌธ์ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์ฌ์ฉ์์ ์ง๋ฌธ์ ์ ํํ๊ณ ๊ฐ๊ฒฐํ๊ฒ ๋ตํ์ธ์.
|
| 108 |
+
๊ธ์ต ์ํ(์๊ธ/์ ๊ธ, ๋์ถ, ํ๋, ๋ณดํ, ์นด๋ ๋ฑ)์ ํน์ง, ์กฐ๊ฑด, ๊ธ๋ฆฌ, ํํ ๋ฑ์ ๋ช
ํํ ์ค๋ช
ํ์ธ์.
|
| 109 |
+
์ฌ์ฉ์ ํ๋กํ์ด ์ ๊ณต๋ ๊ฒฝ์ฐ, ํด๋น ์ ๋ณด๋ฅผ ๊ณ ๋ คํ์ฌ ๋ง์ถคํ ๋ต๋ณ์ ์ ๊ณตํ์ธ์.
|
| 110 |
+
|
| 111 |
+
[๋ฌธ์]
|
| 112 |
+
{context}
|
| 113 |
+
{profile_text}
|
| 114 |
+
[์ง๋ฌธ]
|
| 115 |
+
{question}
|
| 116 |
+
"""
|
| 117 |
+
client = _client_lazy()
|
| 118 |
+
resp = client.chat.completions.create(
|
| 119 |
+
model=OPENAI_MODEL,
|
| 120 |
+
messages=[{"role": "user", "content": prompt}],
|
| 121 |
+
temperature=0.2,
|
| 122 |
+
)
|
| 123 |
+
return resp.choices[0].message.content.strip()
|
vector_service.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sentence_transformers import SentenceTransformer
|
| 2 |
+
import faiss, numpy as np, json
|
| 3 |
+
import os, pickle
|
| 4 |
+
|
| 5 |
+
INDEX_PATH = "vector_db/deposit.index"
|
| 6 |
+
META_PATH = "vector_db/deposit_meta.pkl"
|
| 7 |
+
EMB_MODEL = "intfloat/multilingual-e5-base"
|
| 8 |
+
|
| 9 |
+
_emb_model = None
|
| 10 |
+
_index = None
|
| 11 |
+
_docs = None
|
| 12 |
+
|
| 13 |
+
def _lazy_load():
|
| 14 |
+
"""ํ์์ ๋ฒกํฐ DB, ๋ฌธ์ ๋ฉํ๋ฐ์ดํฐ ๋ก๋"""
|
| 15 |
+
global _emb_model, _index, _docs
|
| 16 |
+
|
| 17 |
+
if _emb_model is None:
|
| 18 |
+
# device="cpu"๋ฅผ ๋ช
์ํ์ฌ meta tensor ์ค๋ฅ ๋ฐฉ์ง
|
| 19 |
+
# model_kwargs={"low_cpu_mem_usage": False} ์ถ๊ฐ: meta tensor ์ค๋ฅ ๋ฐฉ์ง
|
| 20 |
+
_emb_model = SentenceTransformer(EMB_MODEL, device="cpu", model_kwargs={"low_cpu_mem_usage": False})
|
| 21 |
+
print("๐ง ์๋ฒ ๋ฉ ๋ชจ๋ธ ๋ก๋ ์๋ฃ")
|
| 22 |
+
|
| 23 |
+
if _index is None:
|
| 24 |
+
if not os.path.exists(INDEX_PATH):
|
| 25 |
+
raise FileNotFoundError(f"โ {INDEX_PATH} not found.")
|
| 26 |
+
_index = faiss.read_index(INDEX_PATH)
|
| 27 |
+
print("๐ฆ ๋ฒกํฐ ์ธ๋ฑ์ค ๋ก๋ ์๋ฃ")
|
| 28 |
+
|
| 29 |
+
if _docs is None:
|
| 30 |
+
if os.path.exists(META_PATH):
|
| 31 |
+
with open(META_PATH, "rb") as f:
|
| 32 |
+
_docs = pickle.load(f)
|
| 33 |
+
print(f"๐ {_docs and len(_docs)}๊ฐ ๋ฌธ์ ๋ฉํ ๋ก๋๋จ (from deposit_meta.pkl)")
|
| 34 |
+
else:
|
| 35 |
+
print("โ ๏ธ ๋ฉํ๋ฐ์ดํฐ ํ์ผ ์์. ๋น ๋ฆฌ์คํธ๋ก ์ด๊ธฐํ")
|
| 36 |
+
_docs = []
|
| 37 |
+
|
| 38 |
+
def search_similar_docs(query, top_k=3):
|
| 39 |
+
"""์ฟผ๋ฆฌ์ ๊ฐ์ฅ ์ ์ฌํ ๋ฌธ์ ๋ฐํ"""
|
| 40 |
+
_lazy_load()
|
| 41 |
+
query_emb = _emb_model.encode([query])
|
| 42 |
+
D, I = _index.search(query_emb, top_k)
|
| 43 |
+
|
| 44 |
+
results = []
|
| 45 |
+
for idx, score in zip(I[0], D[0]):
|
| 46 |
+
if 0 <= idx < len(_docs):
|
| 47 |
+
results.append(_docs[idx])
|
| 48 |
+
print(f"๐ ๋งค์นญ ๋ฌธ์: {_docs[idx].get('meta', {})} | score={score:.4f}")
|
| 49 |
+
|
| 50 |
+
return results
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# return type: bool
|
| 54 |
+
def check_question_validity(question):
|
| 55 |
+
results = search_similar_docs(question, top_k=1)
|
| 56 |
+
return len(results) > 0
|