diff --git a/README.md b/README.md index 1e637f7..2ae905c 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Philo-RAG의 주요 질문-답변 파이프라인은 다음과 같이 작동합 1. **행복에 대한 질문**: "진정한 행복이란 무엇이라고 생각하시나요?" - 결과: AI는 데이터베이스 내의 다양한 철학 서적을 검색하여, '행복'에 대한 여러 철학자들의 통찰력을 바탕으로 실시간 답변을 작성합니다. 2. **윤리적 딜레마 질문**: "인간 관계에서 거짓말은 어떠한 경우에도 정당화될 수 없나요?" - - 결과: 우측 화면에 도덕이나 윤리와 관련된 출처 카드(알라딘 도서 표지 및 메타데이터 선탑재)가 표기되며, 여러 관점을 혼합한 구조적인 답변이 스트리밍 됩니다. + - 결과: 우측 화면에 도덕·윤리 관련 출처 카드(알라딘 도서 표지 및 메타데이터 사전 로드)가 표시되며, 여러 관점을 혼합한 구조적 답변이 스트리밍됩니다. 3. **사회적 질문**: "이상적이고 평등한 국가란 어떤 모습이어야 할까요?" - 결과: 정치/사회 철학과 관련된 도서 메타데이터를 RAG 파이프라인으로 찾아 직설적이고 다각적인 답변을 제시합니다. @@ -163,6 +163,15 @@ The core Q&A pipeline operates as follows: - **Scroll-Responsive Context Sidebar**: If the conversation grows long and the user scrolls up to an older AI response, the left sidebar automatically reads the metadata for that exact message and updates the sources visually (`IntersectionObserver`). - **High-Speed Streaming UX**: Addressing the common latency issue of RAG pipelines by actively streaming tokens the moment the LLM begins generation. +## 💡 Usage Examples + +1. **Question about Happiness**: "What do you think true happiness is?" + - Result: AI searches various philosophical books in the database and streams a real-time answer based on insights from multiple philosophers regarding 'happiness'. +2. **Ethical Dilemma Question**: "Can lying in human relationships ever be justified?" + - Result: Source cards related to morality or ethics (with book covers and metadata pre-loaded) appear on the right pane, while a structured answer combining different perspectives is streamed. +3. **Social Question**: "What should an ideal and egalitarian state look like?" + - Result: Uses the RAG pipeline to locate metadata on political/social philosophy books and provides a direct, multifaceted answer. + --- ## 💻 How to Run (Local Setup) diff --git a/backend/app/core/env_utils.py b/backend/app/core/env_utils.py index d169fa2..ad50678 100644 --- a/backend/app/core/env_utils.py +++ b/backend/app/core/env_utils.py @@ -5,12 +5,15 @@ def parse_gemini_api_keys(env_path: Path) -> list[str]: """ Reads active GEMINI_API_KEY assignments from the given .env file. - Only extracts active assignments and strips inline comments and quotes. - Also falls back to os.environ if no keys are found in the file. + Extracts active assignments and strips inline comments and quotes. + Also merges GEMINI_API_KEYS (comma-separated) and GEMINI_API_KEY + from os.environ with de-duplication, preserving first-seen order. """ + def _normalize_key(value: str) -> str: + return value.strip().strip('"').strip("'") if value else "" api_keys = [] - if env_path.exists(): + if env_path.is_file(): with open(env_path, 'r', encoding='utf-8') as f: content = f.read() # Find all variations of GEMINI_API_KEY assignments @@ -22,14 +25,24 @@ def parse_gemini_api_keys(env_path: Path) -> list[str]: for m in matches: # Remove inline comments and strip quotes m = re.split(r'\s+#', m, 1)[0] - key = m.strip().strip('"').strip("'") + key = _normalize_key(m) if key and key not in api_keys: api_keys.append(key) - # Fallback to os.environ when parsing produced no key or file doesn't exist - if not api_keys: - k = os.getenv("GEMINI_API_KEY") - if k and k not in api_keys: - api_keys.append(k) + # Also check GEMINI_API_KEYS (comma-separated list) from environment variables + # This is highly useful for deployment environments like Render + env_keys_str = os.getenv("GEMINI_API_KEYS") + if env_keys_str: + for k in env_keys_str.split(','): + key = _normalize_key(k) + if key and key not in api_keys: + api_keys.append(key) + + # Also merge single GEMINI_API_KEY from environment (if present) + k = os.getenv("GEMINI_API_KEY") + if k: + key = _normalize_key(k) + if key and key not in api_keys: + api_keys.append(key) return api_keys diff --git a/backend/scripts/generate_book_mapping.py b/backend/scripts/generate_book_mapping.py index a663433..7b23053 100644 --- a/backend/scripts/generate_book_mapping.py +++ b/backend/scripts/generate_book_mapping.py @@ -115,7 +115,8 @@ async def translate_book_info(file_name: str) -> dict: # If all keys exhausted or other error, fallback print(f"LLM Failed for {file_name}, falling back to Kyobo Search...") - name_without_ext = os.path.splitext(file_name)[0] + # file_name is already stem in current call path; only strip explicit .txt when present + name_without_ext = file_name[:-4] if file_name.lower().endswith(".txt") else file_name parts = name_without_ext.rsplit(" by ", 1) fallback_title = parts[0].strip() fallback_author = parts[1].strip() if len(parts) == 2 else "" diff --git a/frontend/app/icon.png b/frontend/app/icon.png new file mode 100644 index 0000000..aeb13ab Binary files /dev/null and b/frontend/app/icon.png differ diff --git a/frontend/components/chat/MessageList.tsx b/frontend/components/chat/MessageList.tsx index 7fa8ae0..13c81cc 100644 --- a/frontend/components/chat/MessageList.tsx +++ b/frontend/components/chat/MessageList.tsx @@ -119,14 +119,24 @@ export function MessageList({ messages, onOpenCitation, onVisibleMessageChange } } else { elementById.current.delete(id); visibleMessages.current.delete(id); - refCallbackById.current.delete(id); } }, []); const getMessageRef = useCallback((id: string) => { let cb = refCallbackById.current.get(id); if (!cb) { - cb = (el) => observeElement(id, el); + const nextCb = (el: HTMLDivElement | null) => { + observeElement(id, el); + if (el === null) { + // Delay cleanup to survive React StrictMode's setup -> cleanup(null) -> setup cycle + Promise.resolve().then(() => { + if (refCallbackById.current.get(id) === nextCb && !elementById.current.has(id)) { + refCallbackById.current.delete(id); + } + }); + } + }; + cb = nextCb; refCallbackById.current.set(id, cb); } return cb; diff --git a/frontend/components/sidebar/ActivePhilosophers.tsx b/frontend/components/sidebar/ActivePhilosophers.tsx index e3bfb1d..c03fa67 100644 --- a/frontend/components/sidebar/ActivePhilosophers.tsx +++ b/frontend/components/sidebar/ActivePhilosophers.tsx @@ -43,7 +43,7 @@ export function ActivePhilosophers({ metadata, activeMetadata = [] }: Props) { : "border-white/10 bg-white/5" }`} > -
+
{meta.scholar}

{meta.school}

- +
);