jeongsoo commited on
Commit
a77d3be
·
1 Parent(s): 7cef4b0

add Grok api

Browse files
Files changed (2) hide show
  1. utils/grok_client.py +268 -0
  2. utils/llm_interface.py +8 -3
utils/grok_client.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Grok API 클라이언트 모듈
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import logging
8
+ import traceback
9
+ from typing import List, Dict, Any, Optional, Union
10
+ from dotenv import load_dotenv
11
+ import requests
12
+
13
+ # 환경 변수 로드
14
+ load_dotenv()
15
+
16
+ # 로거 설정
17
+ logger = logging.getLogger("GrokLLM")
18
+ if not logger.hasHandlers():
19
+ handler = logging.StreamHandler()
20
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
21
+ handler.setFormatter(formatter)
22
+ logger.addHandler(handler)
23
+ logger.setLevel(logging.INFO)
24
+
25
+ class GrokLLM:
26
+ """Grok API 래퍼 클래스"""
27
+
28
+ def __init__(self):
29
+ """Grok LLM 클래스 초기화"""
30
+ self.api_key = os.getenv("GROK_API_KEY")
31
+ self.api_base = os.getenv("GROK_API_BASE", "https://api.x.ai/v1")
32
+ self.model = os.getenv("GROK_MODEL", "grok-3-mini-beta")
33
+
34
+ if not self.api_key:
35
+ logger.warning("Grok API 키가 .env 파일에 설정되지 않았습니다.")
36
+ logger.warning("GROK_API_KEY를 확인하세요.")
37
+ else:
38
+ logger.info("Grok API 키 로드 완료.")
39
+ logger.debug(f"API 기본 URL: {self.api_base}")
40
+ logger.debug(f"기본 모델: {self.model}")
41
+
42
+ def chat_completion(
43
+ self,
44
+ messages: List[Dict[str, str]],
45
+ temperature: float = 0.7,
46
+ max_tokens: int = 1000,
47
+ stream: bool = False,
48
+ **kwargs
49
+ ) -> Dict[str, Any]:
50
+ """
51
+ Grok 채팅 완성 API 호출
52
+
53
+ Args:
54
+ messages: 채팅 메시지 목록
55
+ temperature: 생성 온도 (낮을수록 결정적)
56
+ max_tokens: 생성할 최대 토큰 수
57
+ stream: 스트리밍 응답 활성화 여부
58
+ **kwargs: 추가 API 매개변수
59
+
60
+ Returns:
61
+ API 응답 (딕셔너리)
62
+ """
63
+ if not self.api_key:
64
+ logger.error("API 키가 설정되지 않아 Grok API를 호출할 수 없습니다.")
65
+ raise ValueError("Grok API 키가 설정되지 않았습니다.")
66
+
67
+ try:
68
+ logger.info(f"Grok API 요청 전송 중 (모델: {self.model})")
69
+
70
+ # API 요청 헤더 및 데이터 준비
71
+ headers = {
72
+ "Authorization": f"Bearer {self.api_key}",
73
+ "Content-Type": "application/json"
74
+ }
75
+
76
+ data = {
77
+ "model": self.model,
78
+ "messages": messages,
79
+ "temperature": temperature,
80
+ "max_tokens": max_tokens,
81
+ "stream": stream
82
+ }
83
+
84
+ # 추가 매개변수 병합
85
+ for key, value in kwargs.items():
86
+ if key not in data:
87
+ data[key] = value
88
+
89
+ # API 요청 보내기
90
+ endpoint = f"{self.api_base}/chat/completions"
91
+
92
+ # 디버깅: API 요청 데이터 로깅 (민감 정보 제외)
93
+ debug_data = data.copy()
94
+ debug_data["messages"] = f"[{len(data['messages'])}개 메시지]"
95
+ logger.debug(f"Grok API 요청 데이터: {json.dumps(debug_data)}")
96
+
97
+ response = requests.post(
98
+ endpoint,
99
+ headers=headers,
100
+ json=data,
101
+ timeout=30 # 30초 타임아웃 설정
102
+ )
103
+
104
+ # 응답 상태 코드 확인
105
+ if not response.ok:
106
+ logger.error(f"Grok API 오류: 상태 코드 {response.status_code}")
107
+ logger.error(f"응답 내용: {response.text}")
108
+ return {"error": f"API 오류: 상태 코드 {response.status_code}", "detail": response.text}
109
+
110
+ # 응답 파싱
111
+ try:
112
+ result = response.json()
113
+ logger.debug(f"API 응답 구조: {list(result.keys())}")
114
+ return result
115
+ except json.JSONDecodeError as e:
116
+ logger.error(f"Grok API JSON 파싱 실패: {e}")
117
+ logger.error(f"원본 응답: {response.text[:500]}...")
118
+ return {"error": "API 응답을 파싱할 수 없습니다", "detail": str(e)}
119
+
120
+ except requests.exceptions.RequestException as e:
121
+ logger.error(f"Grok API 요청 실패: {e}")
122
+ return {"error": f"API 요청 실패: {str(e)}"}
123
+ except Exception as e:
124
+ logger.error(f"Grok API 호출 중 예상치 못한 오류: {e}")
125
+ logger.error(traceback.format_exc())
126
+ return {"error": f"예상치 못한 오류: {str(e)}"}
127
+
128
+ def generate(
129
+ self,
130
+ prompt: str,
131
+ system_prompt: Optional[str] = None,
132
+ temperature: float = 0.7,
133
+ max_tokens: int = 1000,
134
+ **kwargs
135
+ ) -> str:
136
+ """
137
+ 간단한 텍스트 생성 인터페이스
138
+
139
+ Args:
140
+ prompt: 사용자 프롬프트
141
+ system_prompt: 시스템 프롬프트 (선택 사항)
142
+ temperature: 생성 온도
143
+ max_tokens: 생성할 최대 토큰 수
144
+ **kwargs: 추가 API 매개변수
145
+
146
+ Returns:
147
+ 생성된 텍스트
148
+ """
149
+ messages = []
150
+
151
+ if system_prompt:
152
+ messages.append({"role": "system", "content": system_prompt})
153
+
154
+ messages.append({"role": "user", "content": prompt})
155
+
156
+ try:
157
+ response = self.chat_completion(
158
+ messages=messages,
159
+ temperature=temperature,
160
+ max_tokens=max_tokens,
161
+ **kwargs
162
+ )
163
+
164
+ # 오류 응답 확인
165
+ if "error" in response:
166
+ logger.error(f"텍스트 생성 중 API 오류: {response['error']}")
167
+ error_detail = response.get("detail", "")
168
+ return f"API 오류: {response['error']} {error_detail}"
169
+
170
+ # 응답 형식 검증
171
+ if 'choices' not in response or not response['choices']:
172
+ logger.error(f"API 응답에 'choices' 필드가 없습니다: {response}")
173
+ return "응답 형식 오류: 생성된 텍스트를 찾을 수 없습니다."
174
+
175
+ # 메시지 컨텐츠 확인
176
+ choice = response['choices'][0]
177
+ if 'message' not in choice or 'content' not in choice['message']:
178
+ logger.error(f"API 응답에 예상 필드가 없습니다: {choice}")
179
+ return "응답 형식 오류: 메시지 내용을 찾을 수 없습니다."
180
+
181
+ generated_text = choice['message']['content'].strip()
182
+ logger.info(f"텍스트 생성 완료 (길이: {len(generated_text)})")
183
+ return generated_text
184
+
185
+ except Exception as e:
186
+ logger.error(f"텍스트 생성 중 예외 발생: {e}")
187
+ logger.error(traceback.format_exc())
188
+ return f"오류 발생: {str(e)}"
189
+
190
+ def rag_generate(
191
+ self,
192
+ query: str,
193
+ context: List[str],
194
+ system_prompt: Optional[str] = None,
195
+ temperature: float = 0.3,
196
+ max_tokens: int = 1000,
197
+ **kwargs
198
+ ) -> str:
199
+ """
200
+ RAG 검색 결과를 활용한 텍스트 생성
201
+
202
+ Args:
203
+ query: 사용자 질의
204
+ context: 검색된 문맥 목록
205
+ system_prompt: 시스템 프롬프트 (선택 사항)
206
+ temperature: 생성 온도
207
+ max_tokens: 생성할 최대 토큰 수
208
+ **kwargs: 추가 API 매개변수
209
+
210
+ Returns:
211
+ 생성된 텍스트
212
+ """
213
+ if not system_prompt:
214
+ system_prompt = """당신은 검색 결과를 기반으로 질문에 답변하는 도우미입니다.
215
+ - 검색 결과는 <context> 태그 안에 제공됩니다.
216
+ - 검색 결과에 답변이 있으면 해당 정보를 사용하여 명확하게 답변하세요.
217
+ - 검색 결과에 답변이 없으면 "검색 결과에 관련 정보가 없습니다"라고 말하세요.
218
+ - 검색 내용을 그대로 복사하지 말고, 자연스러운 한국어로 답변을 작성하세요.
219
+ - 답변은 간결하고 정확하게 제공하세요."""
220
+
221
+ try:
222
+ # 중요: 컨텍스트 길이 제한
223
+ max_context = 10
224
+ if len(context) > max_context:
225
+ logger.warning(f"컨텍스트가 너무 길어 처음 {max_context}개만 사용합니다.")
226
+ context = context[:max_context]
227
+
228
+ # 각 컨텍스트 액세스
229
+ limited_context = []
230
+ for i, doc in enumerate(context):
231
+ # 각 문서를 1000자로 제한
232
+ if len(doc) > 1000:
233
+ logger.warning(f"문서 {i+1}의 길이가 제한되었습니다 ({len(doc)} -> 1000)")
234
+ doc = doc[:1000] + "...(생략)"
235
+ limited_context.append(doc)
236
+
237
+ context_text = "\n\n".join([f"문서 {i+1}: {doc}" for i, doc in enumerate(limited_context)])
238
+
239
+ prompt = f"""질문: {query}
240
+
241
+ <context>
242
+ {context_text}
243
+ </context>
244
+
245
+ 위 검색 결과를 참고하여 질문에 답변해 주세요."""
246
+
247
+ logger.info(f"RAG 프롬프트 생성 완료 (길이: {len(prompt)})")
248
+
249
+ result = self.generate(
250
+ prompt=prompt,
251
+ system_prompt=system_prompt,
252
+ temperature=temperature,
253
+ max_tokens=max_tokens,
254
+ **kwargs
255
+ )
256
+
257
+ # 결과가 오류 메시지인지 확인
258
+ if result.startswith("오류") or result.startswith("API 오류") or result.startswith("응답 형식 오류"):
259
+ logger.error(f"RAG 생성 결과가 오류를 포함합니다: {result}")
260
+ # 좀 더 사용��� 친화적인 오류 메시지 반환
261
+ return "죄송합니다. 현재 응답을 생성하는데 문제가 발생했습니다. 잠시 후 다시 시도해주세요."
262
+
263
+ return result
264
+
265
+ except Exception as e:
266
+ logger.error(f"RAG 텍스트 생성 중 예외 발생: {str(e)}")
267
+ logger.error(traceback.format_exc())
268
+ return "죄송합니다. 응답 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."
utils/llm_interface.py CHANGED
@@ -10,6 +10,7 @@ from dotenv import load_dotenv
10
  # LLM 클라이언트 임포트
11
  from utils.openai_client import OpenAILLM
12
  from utils.deepseek_client import DeepSeekLLM
 
13
 
14
  # 환경 변수 로드
15
  load_dotenv()
@@ -29,19 +30,21 @@ class LLMInterface:
29
  # 지원되는 LLM 목록 (UI에서 표시될 이름과 내부 식별자)
30
  SUPPORTED_LLMS = {
31
  "OpenAI": "openai",
32
- "DeepSeek": "deepseek"
 
33
  }
34
 
35
  def __init__(self, default_llm: str = "openai"):
36
  """LLM 인터페이스 초기화
37
 
38
  Args:
39
- default_llm: 기본 LLM 식별자 ('openai' 또는 'deepseek')
40
  """
41
  # LLM 클라이언트 초기화
42
  self.llm_clients = {
43
  "openai": OpenAILLM(),
44
- "deepseek": DeepSeekLLM()
 
45
  }
46
 
47
  # 기본 LLM 설정 (유효하지 않은 경우 openai로 설정)
@@ -87,6 +90,8 @@ class LLMInterface:
87
  model = self.llm_clients["openai"].model
88
  elif self.current_llm == "deepseek":
89
  model = self.llm_clients["deepseek"].model
 
 
90
 
91
  return {
92
  "name": name,
 
10
  # LLM 클라이언트 임포트
11
  from utils.openai_client import OpenAILLM
12
  from utils.deepseek_client import DeepSeekLLM
13
+ from utils.grok_client import GrokLLM
14
 
15
  # 환경 변수 로드
16
  load_dotenv()
 
30
  # 지원되는 LLM 목록 (UI에서 표시될 이름과 내부 식별자)
31
  SUPPORTED_LLMS = {
32
  "OpenAI": "openai",
33
+ "DeepSeek": "deepseek",
34
+ "Grok": "grok"
35
  }
36
 
37
  def __init__(self, default_llm: str = "openai"):
38
  """LLM 인터페이스 초기화
39
 
40
  Args:
41
+ default_llm: 기본 LLM 식별자 ('openai', 'deepseek', 또는 'grok')
42
  """
43
  # LLM 클라이언트 초기화
44
  self.llm_clients = {
45
  "openai": OpenAILLM(),
46
+ "deepseek": DeepSeekLLM(),
47
+ "grok": GrokLLM()
48
  }
49
 
50
  # 기본 LLM 설정 (유효하지 않은 경우 openai로 설정)
 
90
  model = self.llm_clients["openai"].model
91
  elif self.current_llm == "deepseek":
92
  model = self.llm_clients["deepseek"].model
93
+ elif self.current_llm == "grok":
94
+ model = self.llm_clients["grok"].model
95
 
96
  return {
97
  "name": name,