Spaces:
Running
Running
| """ | |
| Supabase Adapter | |
| ================ | |
| Supabase(PostgreSQL + pgvector) ์ฐ๋ ์ด๋ํฐ. | |
| ํธํ ๋ก์ดํฐ ๋ฐ์ดํฐ์ ์ ์ฅ ๋ฐ ๋ฒกํฐ ๊ฒ์์ ๋ด๋นํฉ๋๋ค. | |
| ํ ์ด๋ธ ๊ตฌ์กฐ (supabase/migrations/20260109_001_kb_schema.sql ์ฐธ์กฐ): | |
| - kb_documents: ๋ฌธ์ ๋ฉํ๋ฐ์ดํฐ + extracted_knowledge (JSONB) | |
| - kb_chunks: ์ฒญํฌ ํ ์คํธ + ๋ฒกํฐ ์๋ฒ ๋ฉ (pgvector 768์ฐจ์) | |
| """ | |
| import os | |
| import json | |
| from typing import Dict, Any, List, Optional | |
| from datetime import datetime, timezone | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| class SupabaseAdapter: | |
| """ | |
| Supabase ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๋ํฐ. | |
| ํ๊ฒฝ๋ณ์: | |
| SUPABASE_URL: Supabase ํ๋ก์ ํธ URL | |
| SUPABASE_KEY: Supabase anon/service_role ํค | |
| """ | |
| def __init__( | |
| self, | |
| url: Optional[str] = None, | |
| key: Optional[str] = None | |
| ): | |
| """ | |
| Args: | |
| url: Supabase URL (์์ผ๋ฉด ํ๊ฒฝ๋ณ์์์ ๋ก๋) | |
| key: Supabase API Key (์์ผ๋ฉด ํ๊ฒฝ๋ณ์์์ ๋ก๋) | |
| """ | |
| from supabase import create_client, Client | |
| self.url = url or os.getenv("SUPABASE_URL") | |
| self.key = key or os.getenv("SUPABASE_KEY") | |
| if not self.url or not self.key: | |
| raise ValueError( | |
| "SUPABASE_URL๊ณผ SUPABASE_KEY๊ฐ ํ์ํฉ๋๋ค. " | |
| "๋ก์ปฌ: .env ํ์ผ์ ํ์ธํ์ธ์. " | |
| "HF Space: Settings > Repository secrets์ ์ค์ ํ์ธ์." | |
| ) | |
| self.client: Client = create_client(self.url, self.key) | |
| self._embeddings = None | |
| def embeddings(self): | |
| """Lazy ๋ก๋ฉ๋ ์๋ฒ ๋ฉ ์์ฑ๊ธฐ. EMBEDDING_MODEL ํ๊ฒฝ๋ณ์๋ก ์ ํ.""" | |
| if self._embeddings is None: | |
| embedding_model = os.getenv("EMBEDDING_MODEL", "bge-m3").lower() | |
| if embedding_model == "gemini": | |
| from .embeddings import GeminiEmbeddings | |
| self._embeddings = GeminiEmbeddings() | |
| else: # ๊ธฐ๋ณธ๊ฐ: bge-m3 | |
| from .bge_embeddings import BGEEmbeddings | |
| self._embeddings = BGEEmbeddings() | |
| return self._embeddings | |
| # ========================================================================= | |
| # ๋ฌธ์ ์ ์ฅ (kb_documents ํ ์ด๋ธ) | |
| # ========================================================================= | |
| def upsert_knowledge( | |
| self, | |
| doc_id: str, | |
| chain: str, | |
| source_file: str, | |
| identity: Dict[str, Any], | |
| extracted_knowledge: Dict[str, Any], | |
| source_info: Optional[Dict[str, Any]] = None, | |
| version_info: Optional[Dict[str, Any]] = None | |
| ) -> Dict[str, Any]: | |
| """ | |
| ์ถ์ถ๋ ์ง์์ Supabase์ ์ ์ฅ/์ ๋ฐ์ดํธ. | |
| Args: | |
| doc_id: ๊ณ ์ ๋ฌธ์ ID | |
| chain: ํธํ ์ฒด์ธ (IHG, MARRIOTT, ACCOR ๋ฑ) | |
| source_file: ์์ค ํ์ผ ๊ฒฝ๋ก | |
| identity: ๋ฌธ์ identity ๋ฉํ๋ฐ์ดํฐ | |
| extracted_knowledge: ์ถ์ถ๋ ์ง์ (JSONB) | |
| source_info: ์์ค ์ ๋ณด (์ ํ์ ) | |
| version_info: ๋ฒ์ ์ ๋ณด (์ ํ์ ) | |
| Returns: | |
| ์ ์ฅ๋ ๋ ์ฝ๋ | |
| """ | |
| record = { | |
| "doc_id": doc_id, | |
| "chain": chain, | |
| "source_file": source_file, | |
| "identity": json.dumps(identity, ensure_ascii=False), | |
| "extracted_knowledge": json.dumps(extracted_knowledge, ensure_ascii=False), | |
| "source_info": json.dumps(source_info or {}, ensure_ascii=False), | |
| "version_info": json.dumps(version_info or {}, ensure_ascii=False), | |
| "updated_at": datetime.now(timezone.utc).isoformat() | |
| } | |
| try: | |
| result = self.client.table("kb_documents").upsert( | |
| record, | |
| on_conflict="doc_id" | |
| ).execute() | |
| return result.data[0] if result.data else record | |
| except Exception as e: | |
| raise RuntimeError(f"๋ฌธ์ ์ ์ฅ ์คํจ ({doc_id}): {e}") from e | |
| def get_knowledge(self, doc_id: str) -> Optional[Dict[str, Any]]: | |
| """๋ฌธ์ ์กฐํ""" | |
| try: | |
| result = self.client.table("kb_documents")\ | |
| .select("*")\ | |
| .eq("doc_id", doc_id)\ | |
| .execute() | |
| if result.data: | |
| record = result.data[0] | |
| # JSONB ํ๋ ํ์ฑ | |
| for field in ["identity", "extracted_knowledge", "source_info", "version_info"]: | |
| if field in record and isinstance(record[field], str): | |
| record[field] = json.loads(record[field]) | |
| return record | |
| return None | |
| except Exception as e: | |
| raise RuntimeError(f"๋ฌธ์ ์กฐํ ์คํจ ({doc_id}): {e}") from e | |
| def list_documents(self, chain: Optional[str] = None) -> List[Dict[str, Any]]: | |
| """๋ฌธ์ ๋ชฉ๋ก ์กฐํ""" | |
| try: | |
| query = self.client.table("kb_documents").select("doc_id, chain, source_file, updated_at") | |
| if chain: | |
| query = query.eq("chain", chain.upper()) | |
| result = query.execute() | |
| return result.data or [] | |
| except Exception as e: | |
| raise RuntimeError(f"๋ฌธ์ ๋ชฉ๋ก ์กฐํ ์คํจ: {e}") from e | |
| # ========================================================================= | |
| # ์ฒญํฌ ์ ์ฅ (kb_chunks ํ ์ด๋ธ + pgvector) | |
| # ========================================================================= | |
| def upsert_chunks( | |
| self, | |
| chunks: List[Dict[str, Any]], | |
| generate_embeddings: bool = True | |
| ) -> int: | |
| """ | |
| ํ ์คํธ ์ฒญํฌ๋ฅผ ๋ฒกํฐ ์๋ฒ ๋ฉ๊ณผ ํจ๊ป ์ ์ฅ. | |
| Args: | |
| chunks: ์ฒญํฌ ๋ฆฌ์คํธ. ๊ฐ ์ฒญํฌ๋ ๋ค์ ํ๋ ํฌํจ: | |
| - chunk_id: ๊ณ ์ ID | |
| - doc_id: ๋ถ๋ชจ ๋ฌธ์ ID | |
| - chain: ํธํ ์ฒด์ธ | |
| - content: ์ฒญํฌ ํ ์คํธ | |
| - metadata: ์ถ๊ฐ ๋ฉํ๋ฐ์ดํฐ (์ ํ์ ) | |
| generate_embeddings: ์๋ฒ ๋ฉ ์์ฑ ์ฌ๋ถ | |
| Returns: | |
| ์ ์ฅ๋ ์ฒญํฌ ์ | |
| """ | |
| if not chunks: | |
| return 0 | |
| records = [] | |
| for chunk in chunks: | |
| record = { | |
| "chunk_id": chunk["chunk_id"], | |
| "doc_id": chunk["doc_id"], | |
| "chain": chunk.get("chain", "UNKNOWN"), | |
| "chunk_type": chunk.get("metadata", {}).get("type"), | |
| "content": chunk["content"], | |
| "metadata": json.dumps(chunk.get("metadata", {}), ensure_ascii=False), | |
| "updated_at": datetime.now(timezone.utc).isoformat() | |
| } | |
| # ์๋ฒ ๋ฉ ์์ฑ | |
| if generate_embeddings and chunk["content"]: | |
| try: | |
| embedding = self.embeddings.embed_text(chunk["content"]) | |
| record["embedding"] = embedding | |
| except Exception as e: | |
| print(f"โ ๏ธ ์๋ฒ ๋ฉ ์์ฑ ์คํจ ({chunk['chunk_id']}): {e}") | |
| record["embedding"] = None | |
| records.append(record) | |
| # ๋ฐฐ์น ์ฒ๋ฆฌ (Supabase timeout ๋ฐฉ์ง) | |
| BATCH_SIZE = 30 # 50 โ 30์ผ๋ก ์ค์ฌ ์์ ์ฑ ํฅ์ | |
| MAX_RETRIES = 3 | |
| BATCH_DELAY = 0.5 # ๋ฐฐ์น ๊ฐ 0.5์ด ๋๋ ์ด | |
| total_saved = 0 | |
| import time | |
| try: | |
| for i in range(0, len(records), BATCH_SIZE): | |
| batch = records[i:i + BATCH_SIZE] | |
| # ์ฌ์๋ ๋ก์ง | |
| for attempt in range(MAX_RETRIES): | |
| try: | |
| result = self.client.table("kb_chunks").upsert( | |
| batch, | |
| on_conflict="chunk_id" | |
| ).execute() | |
| batch_saved = len(result.data) if result.data else len(batch) | |
| total_saved += batch_saved | |
| break # ์ฑ๊ณต ์ ์ฌ์๋ ๋ฃจํ ์ข ๋ฃ | |
| except Exception as e: | |
| if attempt < MAX_RETRIES - 1: | |
| print(f" โ ๏ธ ๋ฐฐ์น ์ ์ฅ ์คํจ (์๋ {attempt + 1}/{MAX_RETRIES}), ์ฌ์๋ ์ค...") | |
| time.sleep(2) # ์ฌ์๋ ์ 2์ด ๋๊ธฐ | |
| else: | |
| raise # ๋ง์ง๋ง ์๋ ์คํจ ์ ์์ธ ์ ํ | |
| # ์งํ ์ํฉ ์ถ๋ ฅ (100๊ฐ๋ง๋ค) | |
| if (i + BATCH_SIZE) % 100 == 0 or i + BATCH_SIZE >= len(records): | |
| print(f" ๐พ {min(i + BATCH_SIZE, len(records))}/{len(records)} ์ฒญํฌ ์ ์ฅ ์๋ฃ") | |
| # ๋ฐฐ์น ๊ฐ ๋๋ ์ด (์๋ฒ ์ฐ๊ฒฐ ์ ์ง) | |
| if i + BATCH_SIZE < len(records): | |
| time.sleep(BATCH_DELAY) | |
| return total_saved | |
| except Exception as e: | |
| raise RuntimeError(f"์ฒญํฌ ์ ์ฅ ์คํจ: {e}") from e | |
| # ========================================================================= | |
| # ๋ฒกํฐ ๊ฒ์ (Semantic Search) | |
| # ========================================================================= | |
| def search_similar( | |
| self, | |
| query: str, | |
| limit: int = 5, | |
| chain: Optional[str] = None, | |
| threshold: float = 0.5 | |
| ) -> List[Dict[str, Any]]: | |
| """ | |
| ์ฟผ๋ฆฌ์ ์ ์ฌํ ์ฒญํฌ๋ฅผ ๋ฒกํฐ ๊ฒ์. | |
| Args: | |
| query: ๊ฒ์ ์ฟผ๋ฆฌ | |
| limit: ๋ฐํํ ์ต๋ ๊ฒฐ๊ณผ ์ | |
| chain: ํน์ ์ฒด์ธ์ผ๋ก ํํฐ๋ง (์ ํ์ ) | |
| threshold: ์ต์ ์ ์ฌ๋ ์๊ณ๊ฐ (0-1) | |
| Returns: | |
| ์ ์ฌํ ์ฒญํฌ ๋ฆฌ์คํธ (์ ์ฌ๋ ์ ์ ํฌํจ) | |
| """ | |
| # ์ฟผ๋ฆฌ ์๋ฒ ๋ฉ ์์ฑ | |
| query_embedding = self.embeddings.embed_query(query) | |
| try: | |
| # Supabase RPC ํจ์ ํธ์ถ (pgvector match_documents) | |
| # ์ฃผ์: ์ด ํจ์๋ Supabase์์ ๋ณ๋๋ก ์์ฑํด์ผ ํจ | |
| result = self.client.rpc( | |
| "match_kb_chunks", | |
| { | |
| "query_embedding": query_embedding, | |
| "match_threshold": threshold, | |
| "match_count": limit, | |
| "filter_chain": chain | |
| } | |
| ).execute() | |
| return result.data or [] | |
| except Exception as e: | |
| # RPC ํจ์๊ฐ ์๋ ๊ฒฝ์ฐ ๋์ฒด ๋ฐฉ๋ฒ (๋นํจ์จ์ ) | |
| print(f"โ ๏ธ match_kb_chunks RPC ํจ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค: {e}") | |
| print(" supabase/migrations/20260109_001_kb_schema.sql์ ์ฐธ์กฐํ์ธ์.") | |
| return [] | |
| # ========================================================================= | |
| # ์ ํธ๋ฆฌํฐ | |
| # ========================================================================= | |
| def health_check(self) -> Dict[str, Any]: | |
| """์ฐ๊ฒฐ ์ํ ํ์ธ""" | |
| try: | |
| # ๊ฐ๋จํ ์ฟผ๋ฆฌ๋ก ์ฐ๊ฒฐ ํ ์คํธ | |
| result = self.client.table("kb_documents")\ | |
| .select("count", count="exact")\ | |
| .limit(1)\ | |
| .execute() | |
| return { | |
| "status": "healthy", | |
| "documents_count": result.count, | |
| "url": self.url[:30] + "..." | |
| } | |
| except Exception as e: | |
| return { | |
| "status": "error", | |
| "error": str(e) | |
| } | |
| def get_stats(self) -> Dict[str, Any]: | |
| """๋ฐ์ดํฐ๋ฒ ์ด์ค ํต๊ณ""" | |
| try: | |
| docs = self.client.table("kb_documents")\ | |
| .select("chain", count="exact")\ | |
| .execute() | |
| chunks = self.client.table("kb_chunks")\ | |
| .select("chain", count="exact")\ | |
| .execute() | |
| return { | |
| "documents": docs.count or 0, | |
| "chunks": chunks.count or 0 | |
| } | |
| except Exception as e: | |
| return {"error": str(e)} | |
| # ========================================================================= | |
| # ์ฌ์ฉ์ ํ๋กํ (User Gate) | |
| # ========================================================================= | |
| def get_user_profile(self, user_id: str) -> Optional[Dict[str, Any]]: | |
| """ | |
| ์ฌ์ฉ์ ํ๋กํ ์กฐํ. | |
| Args: | |
| user_id: Supabase Auth UUID | |
| Returns: | |
| ํ๋กํ ์ ๋ณด ๋๋ None | |
| """ | |
| try: | |
| result = self.client.table("user_profiles")\ | |
| .select("*")\ | |
| .eq("user_id", user_id)\ | |
| .execute() | |
| return result.data[0] if result.data else None | |
| except Exception as e: | |
| print(f"โ ๏ธ ํ๋กํ ์กฐํ ์คํจ ({user_id}): {e}") | |
| return None | |
| def upsert_user_profile( | |
| self, | |
| user_id: str, | |
| preferred_airports: List[str] = None, | |
| display_name: str = None | |
| ) -> Optional[Dict[str, Any]]: | |
| """ | |
| ์ฌ์ฉ์ ํ๋กํ ์์ฑ/์์ . | |
| Args: | |
| user_id: Supabase Auth UUID | |
| preferred_airports: ์ ํธ ๊ณตํญ ์ฝ๋ ๋ฆฌ์คํธ | |
| display_name: ํ์ ์ด๋ฆ | |
| Returns: | |
| ์ ์ฅ๋ ํ๋กํ ๋๋ None | |
| """ | |
| try: | |
| data = {"user_id": user_id} | |
| if preferred_airports is not None: | |
| data["preferred_airports"] = preferred_airports | |
| if display_name is not None: | |
| data["display_name"] = display_name | |
| result = self.client.table("user_profiles")\ | |
| .upsert(data, on_conflict="user_id")\ | |
| .execute() | |
| return result.data[0] if result.data else None | |
| except Exception as e: | |
| print(f"โ ๏ธ ํ๋กํ ์ ์ฅ ์คํจ ({user_id}): {e}") | |
| return None | |
| # ========================================================================= | |
| # ๋ฉค๋ฒ์ญ (User Gate) | |
| # ========================================================================= | |
| def get_user_memberships(self, user_id: str) -> List[Dict[str, Any]]: | |
| """ | |
| ์ฌ์ฉ์์ ๋ชจ๋ ๋ฉค๋ฒ์ญ ์กฐํ. | |
| Args: | |
| user_id: Supabase Auth UUID | |
| Returns: | |
| ๋ฉค๋ฒ์ญ ๋ฆฌ์คํธ | |
| """ | |
| try: | |
| result = self.client.table("user_memberships")\ | |
| .select("*")\ | |
| .eq("user_id", user_id)\ | |
| .execute() | |
| return result.data or [] | |
| except Exception as e: | |
| print(f"โ ๏ธ ๋ฉค๋ฒ์ญ ์กฐํ ์คํจ ({user_id}): {e}") | |
| return [] | |
| def upsert_membership( | |
| self, | |
| user_id: str, | |
| chain: str, | |
| tier: str, | |
| expires_at: str = None | |
| ) -> Optional[Dict[str, Any]]: | |
| """ | |
| ๋ฉค๋ฒ์ญ ๋ฑ๋ก/์์ . | |
| Args: | |
| user_id: Supabase Auth UUID | |
| chain: ํธํ ์ฒด์ธ ์ฝ๋ (HILTON, MARRIOTT ๋ฑ) | |
| tier: ๋ฑ๊ธ (Gold, Platinum ๋ฑ) | |
| expires_at: ๋ง๋ฃ์ผ (์ ํ, YYYY-MM-DD) | |
| Returns: | |
| ์ ์ฅ๋ ๋ฉค๋ฒ์ญ ๋๋ None | |
| """ | |
| try: | |
| data = { | |
| "user_id": user_id, | |
| "chain": chain.upper(), | |
| "tier": tier | |
| } | |
| if expires_at: | |
| data["expires_at"] = expires_at | |
| # user_id + chain ๋ณตํฉํค๋ก upsert | |
| # Supabase๋ on_conflict์ ๋ณตํฉํค๋ฅผ ์ง์ ์ง์ํ์ง ์์ผ๋ฏ๋ก | |
| # ๋จผ์ ์ญ์ ํ ์ฝ์ ํ๋ ๋ฐฉ์ ์ฌ์ฉ | |
| self.client.table("user_memberships")\ | |
| .delete()\ | |
| .eq("user_id", user_id)\ | |
| .eq("chain", chain.upper())\ | |
| .execute() | |
| result = self.client.table("user_memberships")\ | |
| .insert(data)\ | |
| .execute() | |
| return result.data[0] if result.data else None | |
| except Exception as e: | |
| print(f"โ ๏ธ ๋ฉค๋ฒ์ญ ์ ์ฅ ์คํจ ({user_id}, {chain}): {e}") | |
| return None | |
| def delete_membership(self, user_id: str, chain: str) -> bool: | |
| """ | |
| ๋ฉค๋ฒ์ญ ์ญ์ . | |
| Args: | |
| user_id: Supabase Auth UUID | |
| chain: ํธํ ์ฒด์ธ ์ฝ๋ | |
| Returns: | |
| ์ญ์ ์ฑ๊ณต ์ฌ๋ถ | |
| """ | |
| try: | |
| self.client.table("user_memberships")\ | |
| .delete()\ | |
| .eq("user_id", user_id)\ | |
| .eq("chain", chain.upper())\ | |
| .execute() | |
| return True | |
| except Exception as e: | |
| print(f"โ ๏ธ ๋ฉค๋ฒ์ญ ์ญ์ ์คํจ ({user_id}, {chain}): {e}") | |
| return False | |
| # ========================================================================= | |
| # ์ ์ฉ์นด๋ (Credit Cards) | |
| # ========================================================================= | |
| def get_user_credit_cards(self, user_id: str) -> List[Dict[str, Any]]: | |
| """ | |
| ์ฌ์ฉ์์ ๋ณด์ ์ ์ฉ์นด๋ ๋ชฉ๋ก ์กฐํ. | |
| Args: | |
| user_id: Supabase Auth UUID | |
| Returns: | |
| ์ ์ฉ์นด๋ ๋ฆฌ์คํธ | |
| """ | |
| try: | |
| result = self.client.table("user_credit_cards")\ | |
| .select("*")\ | |
| .eq("user_id", user_id)\ | |
| .eq("is_active", True)\ | |
| .order("created_at")\ | |
| .execute() | |
| return result.data or [] | |
| except Exception as e: | |
| print(f"โ ๏ธ ์ ์ฉ์นด๋ ์กฐํ ์คํจ ({user_id}): {e}") | |
| return [] | |
| def upsert_credit_card( | |
| self, | |
| user_id: str, | |
| card_id: str, | |
| card_name: str, | |
| issuer_code: str, | |
| region: str = "USA", | |
| card_open_date: str = None, | |
| anniversary_month: int = None, | |
| annual_fee: float = None | |
| ) -> Optional[Dict[str, Any]]: | |
| """ | |
| ์ ์ฉ์นด๋ ๋ฑ๋ก/์์ . | |
| Args: | |
| user_id: Supabase Auth UUID | |
| card_id: ์นด๋ ๊ณ ์ ID (AMEX_PLATINUM_US ๋ฑ) | |
| card_name: ์นด๋ ์ด๋ฆ | |
| issuer_code: ๋ฐ๊ธ์ฌ ์ฝ๋ | |
| region: ๋ฐ๊ธ ๊ตญ๊ฐ | |
| card_open_date: ์นด๋ ๊ฐ์ค์ผ (YYYY-MM-DD) | |
| anniversary_month: ์ฐํ๋น ๊ฐฑ์ ์ (1-12) | |
| annual_fee: ์ฐํ๋น | |
| Returns: | |
| ์ ์ฅ๋ ์นด๋ ์ ๋ณด ๋๋ None | |
| """ | |
| try: | |
| data = { | |
| "user_id": user_id, | |
| "card_id": card_id.upper(), | |
| "card_name": card_name, | |
| "issuer_code": issuer_code.upper(), | |
| "region": region.upper(), | |
| "is_active": True | |
| } | |
| if card_open_date: | |
| data["card_open_date"] = card_open_date | |
| if anniversary_month: | |
| data["anniversary_month"] = anniversary_month | |
| if annual_fee is not None: | |
| data["annual_fee_amount"] = annual_fee | |
| # user_id + card_id ๋ณตํฉํค๋ก upsert | |
| self.client.table("user_credit_cards")\ | |
| .delete()\ | |
| .eq("user_id", user_id)\ | |
| .eq("card_id", card_id.upper())\ | |
| .execute() | |
| result = self.client.table("user_credit_cards")\ | |
| .insert(data)\ | |
| .execute() | |
| return result.data[0] if result.data else None | |
| except Exception as e: | |
| print(f"โ ๏ธ ์ ์ฉ์นด๋ ์ ์ฅ ์คํจ ({user_id}, {card_id}): {e}") | |
| return None | |
| def delete_credit_card(self, user_id: str, card_id: str) -> bool: | |
| """ | |
| ์ ์ฉ์นด๋ ์ญ์ (soft delete - is_active = False). | |
| Args: | |
| user_id: Supabase Auth UUID | |
| card_id: ์นด๋ ๊ณ ์ ID | |
| Returns: | |
| ์ญ์ ์ฑ๊ณต ์ฌ๋ถ | |
| """ | |
| try: | |
| self.client.table("user_credit_cards")\ | |
| .update({"is_active": False})\ | |
| .eq("user_id", user_id)\ | |
| .eq("card_id", card_id.upper())\ | |
| .execute() | |
| return True | |
| except Exception as e: | |
| print(f"โ ๏ธ ์ ์ฉ์นด๋ ์ญ์ ์คํจ ({user_id}, {card_id}): {e}") | |
| return False | |
| # ========================================================================= | |
| # ํฌ๋ ๋ง ์ฌ์ฉ ์ถ์ (Credit Usage Tracking) | |
| # ========================================================================= | |
| def get_user_credit_usage( | |
| self, | |
| user_id: str, | |
| card_id: str = None, | |
| benefit_id: str = None | |
| ) -> List[Dict[str, Any]]: | |
| """ | |
| ์ฌ์ฉ์์ ํฌ๋ ๋ง ์ฌ์ฉ ๊ธฐ๋ก ์กฐํ. | |
| Args: | |
| user_id: Supabase Auth UUID | |
| card_id: ํน์ ์นด๋๋ก ํํฐ๋ง (์ ํ) | |
| benefit_id: ํน์ ํํ์ผ๋ก ํํฐ๋ง (์ ํ) | |
| Returns: | |
| ์ฌ์ฉ ๊ธฐ๋ก ๋ฆฌ์คํธ | |
| """ | |
| try: | |
| query = self.client.table("user_credit_usage")\ | |
| .select("*")\ | |
| .eq("user_id", user_id) | |
| if card_id: | |
| query = query.eq("card_id", card_id.upper()) | |
| if benefit_id: | |
| query = query.eq("benefit_id", benefit_id.lower()) | |
| result = query.order("usage_period", desc=True).execute() | |
| return result.data or [] | |
| except Exception as e: | |
| print(f"โ ๏ธ ํฌ๋ ๋ง ์ฌ์ฉ ์กฐํ ์คํจ ({user_id}): {e}") | |
| return [] | |
| def upsert_credit_usage( | |
| self, | |
| user_id: str, | |
| card_id: str, | |
| benefit_id: str, | |
| usage_period: str, | |
| amount_used: float, | |
| amount_limit: float, | |
| currency: str = "USD", | |
| usage_date: str = None, | |
| description: str = None | |
| ) -> Optional[Dict[str, Any]]: | |
| """ | |
| ํฌ๋ ๋ง ์ฌ์ฉ ๊ธฐ๋ก ์ ์ฅ/์์ . | |
| Args: | |
| user_id: Supabase Auth UUID | |
| card_id: ์นด๋ ID | |
| benefit_id: ํํ ID | |
| usage_period: ์ฌ์ฉ ๊ธฐ๊ฐ (2026-H1, 2026-Q1, 2026-01 ๋ฑ) | |
| amount_used: ์ฌ์ฉ ๊ธ์ก | |
| amount_limit: ํ๋ ๊ธ์ก | |
| currency: ํตํ | |
| usage_date: ์ค์ ์ฌ์ฉ์ผ (YYYY-MM-DD) | |
| description: ์ฌ์ฉ ๋ด์ญ | |
| Returns: | |
| ์ ์ฅ๋ ๊ธฐ๋ก ๋๋ None | |
| """ | |
| try: | |
| data = { | |
| "user_id": user_id, | |
| "card_id": card_id.upper(), | |
| "benefit_id": benefit_id.lower(), | |
| "usage_period": usage_period, | |
| "amount_used": amount_used, | |
| "amount_limit": amount_limit, | |
| "currency": currency | |
| } | |
| if usage_date: | |
| data["usage_date"] = usage_date | |
| if description: | |
| data["description"] = description | |
| # user_id + card_id + benefit_id + usage_period ๋ณตํฉํค๋ก upsert | |
| self.client.table("user_credit_usage")\ | |
| .delete()\ | |
| .eq("user_id", user_id)\ | |
| .eq("card_id", card_id.upper())\ | |
| .eq("benefit_id", benefit_id.lower())\ | |
| .eq("usage_period", usage_period)\ | |
| .execute() | |
| result = self.client.table("user_credit_usage")\ | |
| .insert(data)\ | |
| .execute() | |
| return result.data[0] if result.data else None | |
| except Exception as e: | |
| print(f"โ ๏ธ ํฌ๋ ๋ง ์ฌ์ฉ ์ ์ฅ ์คํจ ({user_id}, {card_id}, {benefit_id}): {e}") | |
| return None | |
| def delete_credit_usage( | |
| self, | |
| user_id: str, | |
| card_id: str, | |
| benefit_id: str, | |
| usage_period: str | |
| ) -> bool: | |
| """ | |
| ํฌ๋ ๋ง ์ฌ์ฉ ๊ธฐ๋ก ์ญ์ . | |
| Args: | |
| user_id: Supabase Auth UUID | |
| card_id: ์นด๋ ID | |
| benefit_id: ํํ ID | |
| usage_period: ์ฌ์ฉ ๊ธฐ๊ฐ | |
| Returns: | |
| ์ญ์ ์ฑ๊ณต ์ฌ๋ถ | |
| """ | |
| try: | |
| self.client.table("user_credit_usage")\ | |
| .delete()\ | |
| .eq("user_id", user_id)\ | |
| .eq("card_id", card_id.upper())\ | |
| .eq("benefit_id", benefit_id.lower())\ | |
| .eq("usage_period", usage_period)\ | |
| .execute() | |
| return True | |
| except Exception as e: | |
| print(f"โ ๏ธ ํฌ๋ ๋ง ์ฌ์ฉ ์ญ์ ์คํจ: {e}") | |
| return False | |
| # ========================================================================= | |
| # Valuation Preferences (๊ฐ์น ํ๊ฐ ์ค์ ) | |
| # ========================================================================= | |
| def get_valuation_preferences(self, user_id: str) -> Optional[Dict[str, Any]]: | |
| """ | |
| ์ฌ์ฉ์์ ๊ฐ์น ํ๊ฐ ์ค์ ์กฐํ. | |
| Args: | |
| user_id: Supabase Auth UUID | |
| Returns: | |
| ์ค์ ์ ๋ณด ๋๋ None | |
| """ | |
| try: | |
| result = self.client.table("user_valuation_preferences")\ | |
| .select("*")\ | |
| .eq("user_id", user_id)\ | |
| .execute() | |
| return result.data[0] if result.data else None | |
| except Exception as e: | |
| print(f"โ ๏ธ ๊ฐ์น ํ๊ฐ ์ค์ ์กฐํ ์คํจ ({user_id}): {e}") | |
| return None | |
| def upsert_valuation_preferences( | |
| self, | |
| user_id: str, | |
| travel_style: str = "VALUE", | |
| custom_valuations: Optional[Dict[str, float]] = None | |
| ) -> Optional[Dict[str, Any]]: | |
| """ | |
| ์ฌ์ฉ์์ ๊ฐ์น ํ๊ฐ ์ค์ ์ ์ฅ (Upsert). | |
| Args: | |
| user_id: Supabase Auth UUID | |
| travel_style: ์ฌํ ์คํ์ผ (PREMIUM, VALUE, CASHBACK) | |
| custom_valuations: ๊ฐ๋ณ ํ๋ก๊ทธ๋จ ๊ฐ์น ์ค๋ฒ๋ผ์ด๋ | |
| Returns: | |
| ์ ์ฅ๋ ์ค์ ๋๋ None | |
| """ | |
| try: | |
| data = { | |
| "user_id": user_id, | |
| "travel_style": travel_style.upper(), | |
| "custom_valuations": custom_valuations or {} | |
| } | |
| # user_id UNIQUE ์ ์ฝ์ผ๋ก upsert | |
| self.client.table("user_valuation_preferences")\ | |
| .delete()\ | |
| .eq("user_id", user_id)\ | |
| .execute() | |
| result = self.client.table("user_valuation_preferences")\ | |
| .insert(data)\ | |
| .execute() | |
| return result.data[0] if result.data else None | |
| except Exception as e: | |
| print(f"โ ๏ธ ๊ฐ์น ํ๊ฐ ์ค์ ์ ์ฅ ์คํจ ({user_id}): {e}") | |
| return None | |
| # ============================================================================= | |
| # Supabase SQL ์ค์ (์ฐธ๊ณ ์ฉ) | |
| # ============================================================================= | |
| SUPABASE_SETUP_SQL = """ | |
| -- 1. pgvector ํ์ฅ ํ์ฑํ | |
| CREATE EXTENSION IF NOT EXISTS vector; | |
| -- 2. knowledge_documents ํ ์ด๋ธ | |
| CREATE TABLE IF NOT EXISTS knowledge_documents ( | |
| id SERIAL PRIMARY KEY, | |
| doc_id TEXT UNIQUE NOT NULL, | |
| chain TEXT NOT NULL, | |
| source_file TEXT, | |
| identity JSONB, | |
| extracted_knowledge JSONB, | |
| source_info JSONB, | |
| version_info JSONB, | |
| created_at TIMESTAMPTZ DEFAULT NOW(), | |
| updated_at TIMESTAMPTZ DEFAULT NOW() | |
| ); | |
| CREATE INDEX idx_knowledge_chain ON knowledge_documents(chain); | |
| CREATE INDEX idx_knowledge_doc_id ON knowledge_documents(doc_id); | |
| -- 3. doc_chunks ํ ์ด๋ธ (๋ฒกํฐ ์ ์ฅ) | |
| CREATE TABLE IF NOT EXISTS doc_chunks ( | |
| id SERIAL PRIMARY KEY, | |
| chunk_id TEXT UNIQUE NOT NULL, | |
| doc_id TEXT REFERENCES knowledge_documents(doc_id), | |
| chain TEXT NOT NULL, | |
| content TEXT NOT NULL, | |
| metadata JSONB, | |
| embedding vector(768), -- Gemini text-embedding-004 = 768์ฐจ์ | |
| created_at TIMESTAMPTZ DEFAULT NOW(), | |
| updated_at TIMESTAMPTZ DEFAULT NOW() | |
| ); | |
| CREATE INDEX idx_chunks_doc_id ON doc_chunks(doc_id); | |
| CREATE INDEX idx_chunks_chain ON doc_chunks(chain); | |
| -- 4. ๋ฒกํฐ ๊ฒ์์ฉ ์ธ๋ฑ์ค (HNSW - ๊ถ์ฅ) | |
| CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops); | |
| -- 5. ๋ฒกํฐ ๊ฒ์ RPC ํจ์ | |
| CREATE OR REPLACE FUNCTION match_chunks( | |
| query_embedding vector(768), | |
| match_threshold float, | |
| match_count int, | |
| filter_chain text DEFAULT NULL | |
| ) | |
| RETURNS TABLE ( | |
| chunk_id text, | |
| doc_id text, | |
| chain text, | |
| content text, | |
| metadata jsonb, | |
| similarity float | |
| ) | |
| LANGUAGE plpgsql | |
| AS $$ | |
| BEGIN | |
| RETURN QUERY | |
| SELECT | |
| dc.chunk_id, | |
| dc.doc_id, | |
| dc.chain, | |
| dc.content, | |
| dc.metadata, | |
| 1 - (dc.embedding <=> query_embedding) AS similarity | |
| FROM doc_chunks dc | |
| WHERE | |
| (filter_chain IS NULL OR dc.chain = filter_chain) | |
| AND 1 - (dc.embedding <=> query_embedding) > match_threshold | |
| ORDER BY dc.embedding <=> query_embedding | |
| LIMIT match_count; | |
| END; | |
| $$; | |
| """ | |
| if __name__ == "__main__": | |
| print("๐๏ธ Supabase Adapter ํ ์คํธ") | |
| print("=" * 50) | |
| try: | |
| adapter = SupabaseAdapter() | |
| health = adapter.health_check() | |
| print(f"์ํ: {health['status']}") | |
| if health['status'] == 'healthy': | |
| print(f"๋ฌธ์ ์: {health.get('documents_count', 'N/A')}") | |
| print("โ ์ฐ๊ฒฐ ์ฑ๊ณต") | |
| else: | |
| print(f"์ค๋ฅ: {health.get('error')}") | |
| except Exception as e: | |
| print(f"โ ์ด๊ธฐํ ์คํจ: {e}") | |
| print("\n๐ Supabase ์ค์ SQL:") | |
| print(SUPABASE_SETUP_SQL[:500] + "...") | |