dongchan21 commited on
Commit
bb56df9
ยท
verified ยท
1 Parent(s): efc925f

Upload 2 files

Browse files
Files changed (2) hide show
  1. llm_service.py +123 -0
  2. 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