forked from Downy-newlearner/Perfect_Quote
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
846 lines (711 loc) · 33.8 KB
/
app.py
File metadata and controls
846 lines (711 loc) · 33.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
from flask import Flask, request, jsonify
from flask_cors import CORS
from datetime import datetime
import json
import uuid
import time
import random
import threading
# LangGraph imports
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_upstage import ChatUpstage
from langchain_community.chat_message_histories import ChatMessageHistory
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, List, Dict, Any, Annotated, Optional
import os
from dotenv import load_dotenv
# 시스템 프롬프트 import
from utils.system_prompt import SYSTEM_PROMPT
from utils.analysis_prompt import ANALYSIS_PROMPT
# 명언 검색 시스템
try:
from utils.quote_retriever import find_similar_quote_cosine_silent
print("✅ 명언 검색 시스템 로드 완료")
QUOTE_RETRIEVER_AVAILABLE = True
except ImportError as e:
print(f"⚠️ 명언 검색 시스템 로드 실패: {e}")
QUOTE_RETRIEVER_AVAILABLE = False
# 임베딩 기반 검색을 위한 imports (조건부)
import pandas as pd
import numpy as np
# .env 파일 로드
load_dotenv()
# === 상수 정의 ===
TURN_THRESHOLD = 20
EMBEDDING_AVAILABLE = False
EMBEDDING_LOADING = False
EMBEDDING_LIBS_AVAILABLE = True
# === 기본 명언 데이터 ===
FALLBACK_QUOTES = {
'general': [
{"quote": "인생은 우리가 만들어가는 것이다. 어제보다 나은 오늘을 만들자.", "author": "랄프 왈도 에머슨", "category": "성장", "similarity": 0.88},
{"quote": "변화를 두려워하지 마라. 성장의 시작이다.", "author": "보 베넷", "category": "성장", "similarity": 0.85},
{"quote": "지혜는 경험에서 나오고, 경험은 도전에서 나온다.", "author": "오스카 와일드", "category": "지혜", "similarity": 0.82}
],
'success': [
{"quote": "성공은 준비와 기회가 만나는 지점에서 일어난다.", "author": "바비 언저", "category": "성공", "similarity": 0.92},
{"quote": "실패는 성공의 어머니다. 포기하지 말고 계속 도전하라.", "author": "토마스 에디슨", "category": "성공", "similarity": 0.89},
{"quote": "꿈을 향해 나아가라. 목표가 있으면 길이 보인다.", "author": "랄프 왈도 에머슨", "category": "목표", "similarity": 0.87}
],
'hope': [
{"quote": "어둠 속에서도 한 줄기 빛은 찾을 수 있다.", "author": "마틴 루터 킹", "category": "희망", "similarity": 0.90},
{"quote": "모든 어려움은 지나간다. 시간이 최고의 치료제다.", "author": "괴테", "category": "치유", "similarity": 0.87},
{"quote": "고통은 피할 수 없지만, 고통에 대한 고뇌는 선택사항이다.", "author": "하버 딜런", "category": "극복", "similarity": 0.84}
]
}
print("🔧 임베딩 시스템 강제 활성화")
try:
import faiss
from sentence_transformers import SentenceTransformer
print("✅ 임베딩 라이브러리 로드 완료")
except ImportError as e:
print(f"⚠️ 임베딩 라이브러리 로드 실패: {e}")
print("🔄 런타임에 다시 시도할 예정")
app = Flask(__name__)
CORS(app)
# === LangGraph State 정의 ===
class ChatbotState(TypedDict):
# 사용자 정보
user_id: Annotated[str, "User ID"]
thread_num: Annotated[str, "Session ID"]
# 대화 정보
user_message: Annotated[str, "User Message"]
chatbot_message: Annotated[str, "Chatbot Message"]
timestamp: Annotated[str, "Timestamp of the conversation"]
chat_history: Annotated[ChatMessageHistory, "chat history of user and ai"]
status: Annotated[str, "Status of the conversation"]
# 대화 분석 정보
chat_analysis: Annotated[str, "Analysis of the conversation"]
retrieved_quotes_and_authors: Annotated[Dict[str, str] | List[tuple[str, str]], "Retrieved 3 quotes and 3 authors from vector db"]
quote: Annotated[str, "Quote for the conversation"]
author: Annotated[str, "Author of the quote"]
keywords: Annotated[List[str], "Keywords of the conversation"]
advice: Annotated[str, "Advice for the conversation"]
# 명언 선택 기능을 위한 필드들
candidate_quotes: Annotated[List[Dict], "List of candidate quotes with similarity scores"]
current_quote_index: Annotated[int, "Current quote index being presented"]
quote_selection_complete: Annotated[bool, "Whether quote selection is complete"]
quote_selection_mode: Annotated[bool, "Whether in quote selection mode"]
# === 유틸리티 클래스 ===
class LLMChainBuilder:
"""LLM 체인 생성을 위한 통합 클래스"""
@staticmethod
def _init_llm():
return ChatUpstage(
model="solar-pro",
temperature=0.7,
max_tokens=300,
)
@classmethod
def build_chat_chain(cls):
"""일반 채팅용 체인"""
llm = cls._init_llm()
prompt = ChatPromptTemplate.from_messages([
("system", SYSTEM_PROMPT),
("user", "{user_input}")
])
return prompt | llm
@classmethod
def build_analysis_chain(cls):
"""대화 분석용 체인"""
llm = cls._init_llm()
prompt = ChatPromptTemplate.from_messages([
("system", ANALYSIS_PROMPT),
("user", "다음 대화 히스토리를 분석하라. \\n\\n{chat_history}")
])
return prompt | llm
@classmethod
def build_advice_chain(cls):
"""조언 및 키워드 생성용 체인"""
llm = cls._init_llm()
prompt = ChatPromptTemplate.from_messages([
("system", ANALYSIS_PROMPT + "\n\n분석 결과를 바탕으로 다음 두 가지를 제공해줘요.:\n1. 사용자에게 적절한 조언을 해줘요. 사용자에게는 '당신, 그대'라는 2인칭 표현을 사용해요. (최대 세 문장이며 80자 이내로, 문학적이고 감성적인 어투를 사용하여 친절하게 제공해줘요.)\n2. 대화 내용의 키워드 (최대 5개, 쉼표로 구분. 각 키워드의 글자 수는 최대 4자 이내이다. 5자 초과는 금지이다.)\n\n형식:\n조언: [조언 내용]\n키워드: [키워드1, 키워드2, 키워드3]"),
("user", "{chat_history}")
])
return prompt | llm
class QuoteManager:
"""명언 관련 기능을 담당하는 클래스"""
@staticmethod
def clean_author(author_text: str) -> str:
"""author가 '작가명, 도서명' 형태일 때 작가명만 추출"""
if not author_text:
return ""
return author_text.split(',')[0].strip()
@staticmethod
def select_fallback_quotes(analysis_text: str) -> List[Dict]:
"""분석 내용에 따라 적절한 fallback 명언 선택"""
analysis_lower = analysis_text.lower()
if any(word in analysis_lower for word in ['성공', '도전', '목표', '노력']):
return FALLBACK_QUOTES['success']
elif any(word in analysis_lower for word in ['힘들', '어려움', '슬픔', '우울']):
return FALLBACK_QUOTES['hope']
else:
return FALLBACK_QUOTES['general']
@staticmethod
def search_quotes(chat_analysis: str) -> List[Dict]:
"""명언 검색 (벡터 검색 또는 fallback)"""
fallback_quotes = QuoteManager.select_fallback_quotes(chat_analysis)
try:
if QUOTE_RETRIEVER_AVAILABLE:
import warnings
import sys
from io import StringIO
# 모든 출력과 경고 억제
old_stdout = sys.stdout
old_stderr = sys.stderr
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
sys.stdout = StringIO()
sys.stderr = StringIO()
quotes = find_similar_quote_cosine_silent(chat_analysis, top_k=3)
finally:
# 출력 복원
sys.stdout = old_stdout
sys.stderr = old_stderr
# 검색 결과 검증
if quotes and len(quotes) > 0 and all('quote' in q and 'author' in q for q in quotes):
print(f"✅ 명언 검색 성공: {len(quotes)}개 후보")
return quotes
else:
print("⚠️ 명언 검색 결과가 올바르지 않아 기본 명언을 사용합니다.")
return fallback_quotes
else:
print("⚠️ quote_retriever 사용 불가 - 기본 명언 사용")
return fallback_quotes
except Exception as e:
print(f"⚠️ 명언 검색 중 오류 발생: {e}")
print("기본 명언을 사용합니다.")
return fallback_quotes
@staticmethod
def format_quote_message(quote_data: Dict, current_index: int) -> str:
"""명언 제시 메시지 포맷팅"""
quote_text = quote_data["quote"]
author_text = QuoteManager.clean_author(quote_data["author"])
similarity = quote_data.get("similarity", 0)
return f"이 명언으로 결정할까요?\n\n💬 \"{quote_text}\"\n✍️ 저자: {author_text}\n📊 유사도: {similarity:.3f}\n\n(예/아니오)"
class ConversationHelper:
"""대화 관련 유틸리티 함수들"""
@staticmethod
def is_quit_command(user_input: str) -> bool:
"""종료 명령어 확인"""
quit_commands = ['quit', 'exit', '종료']
return any(cmd in user_input.strip().lower() for cmd in quit_commands)
@staticmethod
def parse_advice_response(response_text: str) -> tuple[str, List[str]]:
"""조언 응답 파싱"""
advice = "대화를 통해 행복을 찾아가시길 바랍니다."
keywords = ["대화", "행복", "고민"]
try:
lines = response_text.split('\n')
for line in lines:
if line.startswith('조언:'):
advice = line.replace('조언:', '').strip()
elif line.startswith('키워드:'):
keywords_text = line.replace('키워드:', '').strip()
keywords = [k.strip() for k in keywords_text.split(',')]
except Exception:
pass # 기본값 사용
return advice, keywords
# === LangGraph 노드 함수들 ===
def validate_user_input(state: ChatbotState) -> ChatbotState:
user_input = state["user_message"]
if not isinstance(user_input, str):
raise TypeError("User message must be a string")
user_input = user_input.strip()
if not user_input:
raise ValueError("User message cannot be empty")
if len(user_input) > 150:
raise ValueError("User message cannot be longer than 150 characters")
return {
**state,
"user_message": user_input,
"status": "validated"
}
def chatbot(state: ChatbotState) -> ChatbotState:
# Initialize chat history if empty
chat_history = state["chat_history"]
if not chat_history:
chat_history = ChatMessageHistory()
# Format chat history for prompt if needed
formatted_history = ""
if chat_history.messages:
formatted_history = "\n".join([
f"{'User' if isinstance(msg, HumanMessage) else 'Assistant'}: {msg.content}"
for msg in chat_history.messages[-6:] # 최근 6개 메시지만 사용
])
chain = LLMChainBuilder.build_chat_chain()
response = chain.invoke({
"user_input": f"{formatted_history}\n\nUser: {state['user_message']}" if formatted_history else state["user_message"]
})
return {
**state,
"chatbot_message": str(response.content),
"timestamp": datetime.now().isoformat(),
"status": "completed"
}
def save_history(state: ChatbotState) -> ChatbotState:
chat_history = state["chat_history"]
chat_history.add_messages([
HumanMessage(content=state["user_message"]),
AIMessage(content=state["chatbot_message"])
])
return {
**state,
"chat_history": chat_history
}
def analyze_chat_history(state: ChatbotState) -> ChatbotState:
chat_history = state["chat_history"]
# 사용자가 종료 명령어를 입력했는지 확인
user_input = state.get("user_message", "").strip().lower()
is_quit_command = ConversationHelper.is_quit_command(user_input)
# 대화 턴 수가 TURN_THRESHOLD 이상이거나 종료 명령어가 입력된 경우에만 분석을 진행
if len(chat_history.messages) < TURN_THRESHOLD and not is_quit_command:
raise ValueError(f"Chat history must be at least {TURN_THRESHOLD} messages")
# 분석 체인을 생성하고 실행한다.
analysis_chain = LLMChainBuilder.build_analysis_chain()
analysis_response = analysis_chain.invoke({
"chat_history": str(chat_history)
})
chat_analysis = analysis_response.content
return {
**state,
"chat_analysis": str(chat_analysis)
}
def generate_advice(state: ChatbotState) -> ChatbotState:
"""대화 분석을 바탕으로 사용자에 적합한 조언을 생성한다."""
chain = LLMChainBuilder.build_advice_chain()
chat_analysis = state["chat_analysis"]
result = chain.invoke({"chat_history": chat_analysis})
# 응답 텍스트 파싱
advice, keywords = ConversationHelper.parse_advice_response(str(result.content))
# 명언 검색
retrieved_quotes = QuoteManager.search_quotes(chat_analysis)
return {**state,
"retrieved_quotes_and_authors": retrieved_quotes,
"advice": advice,
"keywords": keywords,
"candidate_quotes": retrieved_quotes,
"current_quote_index": 0,
"quote_selection_complete": False,
"quote_selection_mode": True, # 명언 선택 모드 활성화
"quote": "",
"author": ""
}
def present_quote(state: ChatbotState) -> ChatbotState:
"""현재 인덱스의 명언을 사용자에게 제시한다."""
candidate_quotes = state["candidate_quotes"]
current_index = state["current_quote_index"]
if not candidate_quotes:
return {
**state,
"chatbot_message": "죄송합니다. 추천할 명언을 찾을 수 없어서 대화를 종료하겠습니다.",
"quote_selection_complete": True,
"quote_selection_mode": False
}
# 현재 명언 가져오기
current_quote = candidate_quotes[current_index]
message = QuoteManager.format_quote_message(current_quote, current_index)
return {
**state,
"chatbot_message": message,
"quote_selection_mode": True,
"timestamp": datetime.now().isoformat()
}
def process_quote_selection(state: ChatbotState) -> ChatbotState:
"""사용자의 명언 선택 응답을 처리한다."""
user_input = state["user_message"].strip().lower()
candidate_quotes = state["candidate_quotes"]
current_index = state["current_quote_index"]
print(f"🔄 process_quote_selection 실행 - 사용자 입력: '{user_input}', 현재 인덱스: {current_index}")
if user_input in ['예', 'yes', 'y', '네', '선택']:
# 현재 명언 선택 확정
selected_quote = candidate_quotes[current_index]
final_message = f"✨ 명언 선택이 완료되었습니다! ✨\n\n💬 \"{selected_quote['quote']}\"\n✍️ {QuoteManager.clean_author(selected_quote['author'])}\n\n🎯 맞춤 조언: {state.get('advice', '')}\n\n이 명언이 당신의 마음에 위로가 되기를 바랍니다. 💝"
print(f"✅ 명언 선택 완료: {selected_quote['quote'][:50]}...")
return {
**state,
"quote": selected_quote["quote"],
"author": QuoteManager.clean_author(selected_quote["author"]),
"quote_selection_complete": True,
"quote_selection_mode": False,
"chatbot_message": final_message,
"timestamp": datetime.now().isoformat(),
"status": "quote_selected"
}
elif user_input in ['아니오', 'no', 'n', '아니', '다음']:
# 다음 명언으로 이동 (순환)
next_index = (current_index + 1) % len(candidate_quotes)
next_quote = candidate_quotes[next_index]
message = QuoteManager.format_quote_message(next_quote, next_index)
print(f"🔄 다음 명언으로 이동: 인덱스 {current_index} → {next_index}")
return {
**state,
"current_quote_index": next_index,
"chatbot_message": message,
"quote": next_quote["quote"], # 현재 명언 정보 포함
"author": QuoteManager.clean_author(next_quote["author"]), # 현재 명언 저자 포함
"quote_selection_mode": True,
"quote_selection_complete": False,
"timestamp": datetime.now().isoformat(),
"status": "validated"
}
else:
# 잘못된 입력 - 현재 명언 다시 제시
current_quote = candidate_quotes[current_index]
message = f"죄송해요, '예' 또는 '아니오'로 답해주세요.\n\n{QuoteManager.format_quote_message(current_quote, current_index)}"
print(f"⚠️ 잘못된 입력: '{user_input}' - 현재 명언 다시 제시")
return {
**state,
"chatbot_message": message,
"quote": current_quote["quote"], # 현재 명언 정보 포함
"author": QuoteManager.clean_author(current_quote["author"]), # 현재 명언 저자 포함
"quote_selection_mode": True,
"quote_selection_complete": False,
"timestamp": datetime.now().isoformat(),
"status": "validated"
}
# === 분기 엣지 정의 ===
def should_analyze_chat_history(state: ChatbotState) -> str:
# 사용자가 종료 명령어를 입력한 경우 체크
user_input = state.get("user_message", "").strip().lower()
if ConversationHelper.is_quit_command(user_input):
return f"messages >= {TURN_THRESHOLD}"
if len(state["chat_history"].messages) >= TURN_THRESHOLD:
return f"messages >= {TURN_THRESHOLD}"
else:
return f"messages < {TURN_THRESHOLD}"
def should_continue_quote_selection(state: ChatbotState) -> str:
"""명언 선택을 계속할지 결정하는 분기 함수"""
# 명언 선택이 완료되었는지 확인
if state.get("quote_selection_complete", False):
return "quote_selection_complete"
# candidate_quotes가 있고 quote_selection_mode가 True인 경우
candidate_quotes = state.get("candidate_quotes", [])
quote_selection_mode = state.get("quote_selection_mode", False)
if candidate_quotes and quote_selection_mode:
# 현재 명언 인덱스 확인
current_index = state.get("current_quote_index", 0)
# 명언 선택 모드가 활성화되어 있으면 계속 진행
return "continue_quote_selection"
# 명언 선택 모드가 아니면 완료 처리
return "quote_selection_complete"
def is_quote_selection_input(state: ChatbotState) -> str:
"""사용자 입력이 명언 선택 관련인지 확인"""
# 명언 선택 모드가 아니면 일반 처리
if not state.get("quote_selection_mode", False):
return "regular_chat"
# 명언 선택 모드면 선택 처리
return "quote_selection"
# === LangGraph 워크플로우 구성 ===
workflow = StateGraph(ChatbotState)
# 노드 추가
workflow.add_node("validate_user_input", validate_user_input)
workflow.add_node("chatbot", chatbot)
workflow.add_node("save_history", save_history)
workflow.add_node("analyze_chat_history", analyze_chat_history)
workflow.add_node("generate_advice", generate_advice)
workflow.add_node("present_quote", present_quote)
workflow.add_node("process_quote_selection", process_quote_selection)
# 기본 엣지 연결
workflow.add_edge(START, "validate_user_input")
# 명언 선택 모드 확인 분기
workflow.add_conditional_edges(
"validate_user_input",
is_quote_selection_input,
path_map={
"regular_chat": "chatbot",
"quote_selection": "process_quote_selection"
}
)
workflow.add_edge("chatbot", "save_history")
# 분석 시점 결정 분기
workflow.add_conditional_edges(
"save_history",
should_analyze_chat_history,
path_map={
f"messages >= {TURN_THRESHOLD}": "analyze_chat_history",
f"messages < {TURN_THRESHOLD}": END
}
)
# 분석 → 조언 생성 → 명언 제시
workflow.add_edge("analyze_chat_history", "generate_advice")
# generate_advice 이후 명언 선택 모드 진입
workflow.add_conditional_edges(
"generate_advice",
should_continue_quote_selection,
path_map={
"continue_quote_selection": "present_quote", # 명언 제시
"quote_selection_complete": END # 선택 완료
}
)
workflow.add_edge("present_quote", END) # 명언 제시 후 사용자 입력 대기
# process_quote_selection에서 다음 명언으로 이동하는 경우 처리
workflow.add_conditional_edges(
"process_quote_selection",
should_continue_quote_selection,
path_map={
"continue_quote_selection": "present_quote", # 다음 명언 제시로 이동
"quote_selection_complete": END # 선택 완료 - 워크플로우 종료
}
)
# 그래프 컴파일
graph = workflow.compile()
# === 통합된 챗봇 클래스 ===
class EnhancedSolarChatbot:
def __init__(self):
self._init_state()
print("🚀 Enhanced Solar Chatbot with LangGraph 초기화 완료")
def _init_state(self):
"""상태 초기화"""
self.state = {
"user_id": "",
"thread_num": "",
"user_message": "",
"chatbot_message": "",
"timestamp": "",
"chat_history": ChatMessageHistory(),
"status": "",
"quote": "",
"author": "",
"retrieved_quotes_and_authors": [],
"candidate_quotes": [],
"current_quote_index": 0,
"quote_selection_complete": False,
"quote_selection_mode": False,
"chat_analysis": "",
"keywords": [],
"advice": ""
}
def run_chatbot_once(self, user_input, user_id, thread_num):
"""단일 턴 대화 실행 - 완전히 LangGraph로 통합"""
# 상태 업데이트
self.state["user_message"] = user_input
self.state["user_id"] = user_id
self.state["thread_num"] = thread_num
try:
print(f"🔄 LangGraph 실행 시작 - User: {user_input[:30]}...")
print(f"📊 현재 상태: quote_selection_mode={self.state.get('quote_selection_mode')}, candidate_quotes={len(self.state.get('candidate_quotes', []))}")
# LangGraph로 모든 로직 처리
result = graph.invoke(self.state)
self.state.update(result)
print(f"✅ LangGraph 실행 완료")
print(f"📊 결과 상태: quote_selection_mode={self.state.get('quote_selection_mode')}, quote_selection_complete={self.state.get('quote_selection_complete')}")
print(f"💬 응답: {self.state.get('chatbot_message', '')[:100]}...")
# 디버그 정보 출력
if self.state.get('quote_selection_complete'):
print(f"✅ 명언 선택 완료: {self.state['quote'][:50]}...")
elif self.state.get('quote_selection_mode'):
print(f"🔄 명언 선택 모드 활성 - 인덱스: {self.state.get('current_quote_index', 0)}")
elif self.state.get('advice'):
print(f"🎉 대화 분석 완료 - 명언 선택 시작")
return self.state
except Exception as e:
print(f"❌ 챗봇 실행 오류: {e}")
return {
**self.state,
"chatbot_message": "죄송해요, 지금 대화하는데 문제가 생겼어요. 잠시 후 다시 시도해주시겠어요?",
"status": "error"
}
def get_conversation_summary(self):
"""대화 요약 정보 반환"""
return {
"message_count": len(self.state["chat_history"].messages),
"analysis_ready": len(self.state["chat_history"].messages) >= TURN_THRESHOLD,
"quote_selection_mode": self.state.get("quote_selection_mode", False),
"quote_selected": bool(self.state.get("quote")),
"quote_selection_complete": self.state.get("quote_selection_complete", False),
"advice": self.state.get("advice", ""),
"keywords": self.state.get("keywords", [])
}
# === 세션 관리 ===
chatbot_sessions = {}
session_lock = threading.Lock()
def get_chatbot_instance(user_id, thread_num):
"""사용자별 Enhanced Solar 챗봇 인스턴스 가져오기 또는 생성"""
session_key = f"{user_id}_{thread_num}"
with session_lock:
if session_key not in chatbot_sessions:
chatbot_sessions[session_key] = {
'chatbot': EnhancedSolarChatbot(),
'created_at': datetime.now(),
'last_used': datetime.now()
}
print(f"🚀 새로운 Enhanced Solar 챗봇 세션 생성: {session_key}")
else:
chatbot_sessions[session_key]['last_used'] = datetime.now()
return chatbot_sessions[session_key]['chatbot']
# === API 엔드포인트들 ===
@app.route('/api/health', methods=['GET'])
def health_check():
"""서버 상태 확인"""
global EMBEDDING_LOADING, EMBEDDING_AVAILABLE
# 임베딩 시스템 상태 결정
if EMBEDDING_AVAILABLE:
embedding_status = "✅ ACTIVE"
message = "🎉 Solar API + LangGraph + 개인화 명언 추천 시스템 완전 활성화!"
elif EMBEDDING_LOADING:
embedding_status = "🔄 LOADING"
message = "📥 Solar API + LangGraph 동작 중 + 임베딩 시스템 백그라운드 로딩 중..."
else:
embedding_status = "⚠️ FALLBACK"
message = "🔥 Solar API + LangGraph 동작 중 + 기본 명언 시스템 사용"
return jsonify({
'status': 'OK',
'timestamp': datetime.now().isoformat(),
'activeConversations': len(chatbot_sessions),
'model': 'Solar Pro API + LangGraph',
'embedding_system': embedding_status,
'embedding_available': EMBEDDING_AVAILABLE,
'embedding_loading': EMBEDDING_LOADING,
'quote_retriever_available': QUOTE_RETRIEVER_AVAILABLE,
'message': message
})
@app.route('/api/chat/send', methods=['POST'])
def send_message():
"""메시지 전송 API - LangGraph 기반 Enhanced Solar 챗봇 사용"""
try:
data = request.get_json()
# 필수 필드 확인
required_fields = ['userId', 'threadNum', 'content']
for field in required_fields:
if field not in data:
return jsonify({'error': f'Missing required field: {field}'}), 400
user_id = data['userId']
thread_num = data['threadNum']
content = data['content']
print(f"🤖 Enhanced Solar API 호출 - User: {user_id}, Message: {content}")
# Enhanced Solar 챗봇 인스턴스 가져오기
chatbot = get_chatbot_instance(user_id, thread_num)
# LangGraph로 응답 생성
result_state = chatbot.run_chatbot_once(content, user_id, thread_num)
ai_response = result_state.get('chatbot_message', '응답을 생성할 수 없습니다.')
print(f"✨ Enhanced Solar API 응답: {ai_response}")
# 응답 데이터 구성
response_data = {
'userId': user_id,
'threadNum': thread_num,
'timestamp': result_state.get('timestamp', datetime.now().isoformat()),
'status': result_state.get('status', 'completed'),
'content': ai_response,
'quote': None,
'quote_selection': {
'active': False,
'current_index': 0,
'total_count': 0,
'quote_id': None,
'changed': False
},
'model': 'Solar Pro + LangGraph',
'embedding_system': 'Enhanced FAISS',
'conversation_summary': chatbot.get_conversation_summary()
}
# 명언 선택 모드인 경우
if result_state.get('quote_selection_mode') and result_state.get('candidate_quotes'):
current_index = result_state.get('current_quote_index', 0)
candidate_quotes = result_state.get('candidate_quotes', [])
current_quote = candidate_quotes[current_index] if candidate_quotes else None
if current_quote:
response_data['quote'] = {
'id': str(uuid.uuid4()),
'text': current_quote.get('quote', ''),
'author': QuoteManager.clean_author(current_quote.get('author', '')),
'advice': result_state.get('advice', ''),
'keywords': result_state.get('keywords', []),
'method': 'langgraph_enhanced_selection'
}
response_data['quote_selection'] = {
'active': True,
'current_index': current_index,
'total_count': len(candidate_quotes),
'quote_id': str(uuid.uuid4()),
'changed': True
}
print(f"🔄 명언 선택 모드 활성 - 인덱스: {current_index}/{len(candidate_quotes)}")
print(f"📝 응답 내용: {result_state.get('chatbot_message', '')[:100]}...")
# 명언 선택이 완료된 경우
elif result_state.get('quote_selection_complete') and result_state.get('quote'):
response_data['quote'] = {
'id': str(uuid.uuid4()),
'text': result_state['quote'],
'author': QuoteManager.clean_author(result_state['author']),
'advice': result_state.get('advice', ''),
'keywords': result_state.get('keywords', []),
'method': 'langgraph_enhanced_selection'
}
response_data['quote_selection'] = {
'active': False,
'current_index': 0,
'total_count': 0,
'quote_id': str(uuid.uuid4()),
'changed': False
}
print(f"📜 최종 명언 선택 완료: {result_state['quote'][:50]}... - {QuoteManager.clean_author(result_state['author'])}")
print(f"🎯 조언: {result_state.get('advice', '')}")
print(f"🔑 키워드: {result_state.get('keywords', [])}")
print(f"📝 완료 응답 내용: {result_state.get('chatbot_message', '')[:100]}...")
# TURN_THRESHOLD 턴 분석 완료 시 추가 정보
if len(result_state.get('chat_history', ChatMessageHistory()).messages) >= TURN_THRESHOLD:
if result_state.get('advice'):
response_data['analysis_complete'] = True
response_data['advice'] = result_state.get('advice', '')
response_data['keywords'] = result_state.get('keywords', [])
print(f"🎉 대화 분석 완료 - 조언: {result_state.get('advice', '')}")
return jsonify(response_data)
except Exception as e:
print(f"❌ 에러 발생: {e}")
return jsonify({
'error': str(e),
'status': 'error',
'timestamp': datetime.now().isoformat(),
'model': 'Solar Pro + LangGraph'
}), 500
@app.route('/api/chat/status', methods=['GET'])
def get_status():
"""상태 확인 API (폴링용)"""
try:
user_id = request.args.get('userId')
thread_num = request.args.get('threadNum')
if not user_id or not thread_num:
return jsonify({'error': 'Missing userId or threadNum'}), 400
# 챗봇 인스턴스가 존재하는지 확인
session_key = f"{user_id}_{thread_num}"
if session_key in chatbot_sessions:
chatbot = chatbot_sessions[session_key]['chatbot']
return jsonify({
'userId': user_id,
'threadNum': thread_num,
'timestamp': datetime.now().isoformat(),
'status': 'active',
'model': 'Solar Pro + LangGraph',
'embedding_system': 'Enhanced FAISS',
'conversation_summary': chatbot.get_conversation_summary()
})
else:
return jsonify({
'userId': user_id,
'threadNum': thread_num,
'timestamp': datetime.now().isoformat(),
'status': 'inactive',
'model': 'Solar Pro + LangGraph',
'embedding_system': 'Enhanced FAISS'
})
except Exception as e:
return jsonify({
'error': str(e),
'status': 'error',
'timestamp': datetime.now().isoformat(),
'model': 'Solar Pro + LangGraph'
}), 500
if __name__ == '__main__':
print("🚀 Enhanced Solar API + LangGraph 서버 시작 중...")
print("📡 포트: 3001")
print("🔥 모델: Solar Pro API + LangGraph StateGraph")
print("🧠 임베딩: Enhanced SentenceTransformer + FAISS")
print("📊 명언 검색: utils.quote_retriever")
print("🎯 분석: 대화 내용 분석 + 명언 선택")
print("🔧 디버그 모드: False")
print("🌐 CORS 활성화됨")
print("✨ LangGraph 기반 개인화된 명언 추천 시스템!")
app.run(host='0.0.0.0', port=3001, debug=False, use_reloader=False)