From da79f950b8be42de1576da3c1a1bed52119b8c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=A2=E1=84=92=E1=85=A7?= =?UTF-8?q?=E1=86=A8?= <86944451+choijhyeok@users.noreply.github.com> Date: Sun, 11 Jan 2026 15:04:59 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A4=91=EA=B0=84=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- generate_synthetic_table/__main__.py | 6 + generate_synthetic_table/flow.py | 45 ++- generate_synthetic_table/images/image.png | Bin 0 -> 76831 bytes generate_synthetic_table/notion_uploader.py | 385 ++++++++++++++++++++ generate_synthetic_table/runner.py | 92 +++++ google_drive_download.py | 0 notion_import.csv | 3 + polling_gemini/api_pool.py | 8 +- polling_gemini/langgraph_integration.py | 23 +- pyproject.toml | 1 + uv.lock | 16 +- 11 files changed, 569 insertions(+), 10 deletions(-) create mode 100644 generate_synthetic_table/__main__.py create mode 100644 generate_synthetic_table/images/image.png create mode 100644 generate_synthetic_table/notion_uploader.py create mode 100644 google_drive_download.py create mode 100644 notion_import.csv diff --git a/generate_synthetic_table/__main__.py b/generate_synthetic_table/__main__.py new file mode 100644 index 0000000..44a71f0 --- /dev/null +++ b/generate_synthetic_table/__main__.py @@ -0,0 +1,6 @@ +"""Entry point for running the package as a module.""" + +from .cli import main + +if __name__ == "__main__": + main() diff --git a/generate_synthetic_table/flow.py b/generate_synthetic_table/flow.py index da29452..96d056c 100644 --- a/generate_synthetic_table/flow.py +++ b/generate_synthetic_table/flow.py @@ -36,6 +36,7 @@ class TableState(TypedDict, total=False): errors: List[str] synthetic_json: dict # 파싱된 합성 데이터 JSON qa_results: List[Dict] # 생성된 QA 쌍 + token_usage: int # QA 생성에 사용된 총 토큰 수 def _encode_image(image_path: Path) -> str: @@ -55,8 +56,18 @@ def _encode_image(image_path: Path) -> str: def _call_llm( - llm: ChatOpenAI, prompt: str, image_urls: Optional[List[str]] = None) -> str: - """Call the multi-modal LLM with optional multiple images.""" + llm: ChatOpenAI, prompt: str, image_urls: Optional[List[str]] = None, return_token_usage: bool = False) -> str: + """Call the multi-modal LLM with optional multiple images. + + Args: + llm: The LLM instance + prompt: The text prompt + image_urls: Optional list of image URLs + return_token_usage: If True, returns tuple of (content, total_tokens) + + Returns: + Response content string, or tuple of (content, total_tokens) if return_token_usage=True + """ content: List[Dict] = [{"type": "text", "text": prompt}] @@ -71,7 +82,31 @@ def _call_llm( }) response = llm.invoke([HumanMessage(content=content)]) - return response.content if isinstance(response.content, str) else json.dumps(response.content) + response_content = response.content if isinstance(response.content, str) else json.dumps(response.content) + + if return_token_usage: + # Extract token usage from response metadata + token_usage = 0 + if hasattr(response, 'response_metadata'): + usage_metadata = response.response_metadata.get('usage', {}) + # OpenAI/Gemini format + token_usage = usage_metadata.get('total_tokens', 0) + # Fallback: prompt_tokens + completion_tokens + if not token_usage: + token_usage = usage_metadata.get('prompt_tokens', 0) + usage_metadata.get('completion_tokens', 0) + # Fallback: input_tokens + output_tokens + if not token_usage: + token_usage = usage_metadata.get('input_tokens', 0) + usage_metadata.get('output_tokens', 0) + # Alternative: usage_metadata attribute (dict or object) + if not token_usage and hasattr(response, 'usage_metadata') and response.usage_metadata: + usage = response.usage_metadata + if isinstance(usage, dict): + token_usage = usage.get('total_tokens', 0) or (usage.get('input_tokens', 0) + usage.get('output_tokens', 0)) + else: + token_usage = getattr(usage, 'total_tokens', 0) or (getattr(usage, 'input_tokens', 0) + getattr(usage, 'output_tokens', 0)) + return response_content, token_usage + + return response_content def _load_yaml_prompts(filename: str) -> Dict[str, str]: @@ -566,7 +601,7 @@ def _node(state: TableState) -> TableState: prompt = prompt_template - response_text = _call_llm(llm, prompt, image_urls=image_data_urls) + response_text, token_usage = _call_llm(llm, prompt, image_urls=image_data_urls, return_token_usage=True) response_json = robust_json_parse(response_text) qa_results = [] @@ -575,7 +610,7 @@ def _node(state: TableState) -> TableState: else: logger.warning("QA generation from image did not return valid JSON or 'qa_pairs' key.") - return {**state, "qa_results": qa_results} + return {**state, "qa_results": qa_results, "token_usage": token_usage} return _node diff --git a/generate_synthetic_table/images/image.png b/generate_synthetic_table/images/image.png new file mode 100644 index 0000000000000000000000000000000000000000..ad30805d2cb5738dbe89fe436f56a211b74afeac GIT binary patch literal 76831 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 z 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 Dict[str, Any]: + """Load Notion configuration from gemini_keys.yaml or specified path.""" + if config_path is None: + # Default path + base_dir = Path(__file__).parent.parent + config_path = base_dir / "apis" / "gemini_keys.yaml" + else: + config_path = Path(config_path) + + if not config_path.exists(): + raise FileNotFoundError(f"Config file not found: {config_path}") + + with open(config_path, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) + + return config + + +class NotionUploader: + """Upload QA results to Notion Database.""" + + # Domain -> Database ID 매핑 + DOMAIN_DATABASE_MAP = { + "public": None, # 설정 파일에서 로드 + "insurance": None, + "academic": None, + "business": None, + "finance": None, + "medical": None, + } + + def __init__(self, config_path: Optional[str] = None): + """Initialize Notion client with API key from config.""" + if Client is None: + raise ImportError( + "notion-client package is not installed. " + "Install it with: pip install notion-client" + ) + + config = _load_notion_config(config_path) + + # Get Notion API key + self.api_key = config.get("notion_key") + if not self.api_key: + raise ValueError("notion_key not found in config file") + + # Get database IDs from config + notion_databases = config.get("notion_databases", {}) + for domain, db_id in notion_databases.items(): + if domain in self.DOMAIN_DATABASE_MAP: + self.DOMAIN_DATABASE_MAP[domain] = db_id + + # Initialize Notion client + self.client = Client(auth=self.api_key) + + # Cache for database properties + self._db_properties_cache: Dict[str, Dict] = {} + + def get_database_id(self, domain: str) -> str: + """Get database ID for the given domain.""" + db_id = self.DOMAIN_DATABASE_MAP.get(domain.lower()) + if not db_id: + raise ValueError( + f"No database ID configured for domain '{domain}'. " + f"Please add 'notion_databases.{domain}' to your config file." + ) + return db_id + + def _get_database_properties(self, database_id: str) -> Dict[str, str]: + """Get database properties and their types using direct API call. Cached.""" + if database_id in self._db_properties_cache: + return self._db_properties_cache[database_id] + + # Use direct httpx call to ensure we get properties + # (notion-client may not return properties for some database types) + url = f"https://api.notion.com/v1/databases/{database_id}" + headers = { + "Authorization": f"Bearer {self.api_key}", + "Notion-Version": "2022-06-28", + } + response = httpx.get(url, headers=headers) + db = response.json() + + props = {} + for name, prop in db.get("properties", {}).items(): + props[name] = prop.get("type") + + self._db_properties_cache[database_id] = props + return props + + def _find_title_property(self, db_properties: Dict[str, str]) -> Optional[str]: + """Find the title property name in database.""" + for name, prop_type in db_properties.items(): + if prop_type == "title": + return name + return None # No title property found + + def _ensure_database_properties(self, database_id: str) -> str: + """Ensure the database has required properties, create them if missing. + Returns the title property name.""" + db_properties = self._get_database_properties(database_id) + title_prop = self._find_title_property(db_properties) + + # Check if we need to add properties + properties_to_add = {} + + # Title property is required - if missing, add "Name" + if not title_prop: + properties_to_add["Name"] = {"title": {}} + title_prop = "Name" + + # Add properties for individual QA rows + if "Domain" not in db_properties: + properties_to_add["Domain"] = {"select": {}} + + if "Image" not in db_properties: + properties_to_add["Image"] = {"rich_text": {}} + + if "Question" not in db_properties: + properties_to_add["Question"] = {"rich_text": {}} + + if "Answer" not in db_properties: + properties_to_add["Answer"] = {"rich_text": {}} + + if "Type" not in db_properties: + properties_to_add["Type"] = {"select": {}} + + if "Count" not in db_properties: + properties_to_add["Count"] = {"number": {}} + + if "Token" not in db_properties: + properties_to_add["Token"] = {"number": {}} + + if "Provider" not in db_properties: + properties_to_add["Provider"] = {"select": {}} + + if properties_to_add: + print(f" 📝 Adding missing properties: {list(properties_to_add.keys())}") + self.client.databases.update( + database_id=database_id, + properties=properties_to_add + ) + # Clear cache to refresh + if database_id in self._db_properties_cache: + del self._db_properties_cache[database_id] + + return title_prop + + def upload_qa_result( + self, + domain: str, + image_path: str, + qa_results: List[Dict], + table_summary: Optional[str] = None, + additional_properties: Optional[Dict] = None, + token_usage: int = 0, + provider: str = "unknown", + ) -> Dict[str, Any]: + """ + Upload QA results to Notion database. + Each QA pair is uploaded as a separate row. + + Args: + domain: Domain name (public, insurance, etc.) + image_path: Path to the source image + qa_results: List of QA pairs + table_summary: Optional table summary + additional_properties: Optional additional properties to add + token_usage: Total tokens used for QA generation + provider: LLM provider used (openai, gemini, gemini_pool, claude, vllm) + + Returns: + Summary of created pages + """ + database_id = self.get_database_id(domain) + + # Ensure database has required properties and get title property name + title_prop_name = self._ensure_database_properties(database_id) + + # Get current database properties to check types + db_properties = self._get_database_properties(database_id) + + # Helper function to build property value based on actual type + def build_property_value(prop_name: str, value: str, preferred_type: str = "select") -> Dict: + """Build property value based on actual property type in database.""" + if prop_name not in db_properties: + # Property doesn't exist, use preferred type + if preferred_type == "select": + return {"select": {"name": value}} + else: + return {"rich_text": [{"text": {"content": value[:2000]}}]} + + # db_properties stores type directly as string value + actual_type = db_properties[prop_name] + if actual_type == "select": + return {"select": {"name": value}} + elif actual_type == "multi_select": + return {"multi_select": [{"name": value}]} + else: + # Default to rich_text for any other type + return {"rich_text": [{"text": {"content": value[:2000]}}]} + + # Get just the filename (not full path) + image_filename = Path(image_path).name + + # Calculate tokens per QA (distribute evenly) + num_qa = len(qa_results) + tokens_per_qa = token_usage // num_qa if num_qa > 0 and token_usage > 0 else 0 + + created_pages = [] + + # Create a separate row for each QA pair + for i, qa in enumerate(qa_results, 1): + question = qa.get("question", qa.get("Q", "")) + answer = qa.get("answer", qa.get("A", "")) + qa_type = qa.get("type", "unknown") + + # Build page properties for this QA + properties = { + title_prop_name: { + "title": [ + { + "text": { + "content": Path(image_path).stem + } + } + ] + }, + "Count": { + "number": i + }, + "Domain": { + "select": { + "name": domain.capitalize() + } + }, + "Image": { + "rich_text": [ + { + "text": { + "content": image_filename + } + } + ] + }, + "Question": { + "rich_text": [ + { + "text": { + "content": question[:2000] # Notion limit + } + } + ] + }, + "Answer": { + "rich_text": [ + { + "text": { + "content": answer[:2000] # Notion limit + } + } + ] + }, + "Type": build_property_value("Type", qa_type, "select"), + "Token": { + "number": tokens_per_qa + }, + "Provider": build_property_value("Provider", provider, "select"), + } + + # Create the page (row) + response = self.client.pages.create( + parent={"database_id": database_id}, + properties=properties, + ) + created_pages.append(response) + + return { + "created_count": len(created_pages), + "pages": created_pages + } + + def upload_batch_results( + self, + domain: str, + results: List[Dict], + verbose: bool = True, + provider: str = "unknown", + ) -> Dict[str, Any]: + """ + Upload multiple QA results to Notion database. + + Args: + domain: Domain name + results: List of result dictionaries with image_path, qa_results, etc. + verbose: Print progress messages + provider: LLM provider used + + Returns: + Summary of upload results + """ + success_count = 0 + failed_count = 0 + total_qa_count = 0 + failed_items = [] + + for i, result in enumerate(results, 1): + image_path = result.get("image_path", result.get("name", f"unknown_{i}")) + qa_results = result.get("qa_results", []) + table_summary = result.get("table_summary") + token_usage = result.get("token_usage", 0) + + if not qa_results: + if verbose: + print(f"⏭️ Skipping {image_path} (no QA results)") + continue + + try: + upload_result = self.upload_qa_result( + domain=domain, + image_path=image_path, + qa_results=qa_results, + table_summary=table_summary, + token_usage=token_usage, + provider=provider, + ) + created_count = upload_result.get("created_count", 0) + total_qa_count += created_count + success_count += 1 + if verbose: + print(f"✅ [{i}/{len(results)}] Uploaded: {image_path} ({created_count} QA rows)") + except Exception as e: + failed_count += 1 + failed_items.append({"path": image_path, "error": str(e)}) + if verbose: + print(f"❌ [{i}/{len(results)}] Failed: {image_path} - {e}") + + return { + "total": len(results), + "success": success_count, + "failed": failed_count, + "total_qa_rows": total_qa_count, + "failed_items": failed_items, + } + + +def upload_to_notion( + domain: str, + results: List[Dict], + config_path: Optional[str] = None, + verbose: bool = True, + provider: str = "unknown", +) -> Dict[str, Any]: + """ + Convenience function to upload results to Notion. + + Args: + domain: Domain name (public, insurance, etc.) + results: List of result dictionaries + config_path: Optional path to config file + verbose: Print progress messages + provider: LLM provider used + + Returns: + Upload summary + """ + uploader = NotionUploader(config_path=config_path) + return uploader.upload_batch_results(domain, results, verbose=verbose, provider=provider) diff --git a/generate_synthetic_table/runner.py b/generate_synthetic_table/runner.py index c177f4a..9ea78c8 100644 --- a/generate_synthetic_table/runner.py +++ b/generate_synthetic_table/runner.py @@ -13,6 +13,7 @@ from dotenv import load_dotenv from .flow import TableState, run_synthetic_table_flow +from .notion_uploader import upload_to_notion try: from data_organizer import TableDataOrganizer @@ -110,6 +111,16 @@ def build_arg_parser() -> argparse.ArgumentParser: "--domain", help="Domain for prompt customization (e.g., 'public'). Auto-detected if input starts with 'P_'.", ) + parser.add_argument( + "--upload-notion", + action="store_true", + help="Upload QA results to Notion database (requires domain to be specified)", + ) + parser.add_argument( + "--notion-config", + type=Path, + help="Path to config file with Notion API key and database IDs (default: apis/gemini_keys.yaml)", + ) return parser @@ -203,6 +214,8 @@ def run_with_args(args: argparse.Namespace) -> TableState | Dict: max_k=getattr(args, 'max_k', 3), num_samples=getattr(args, 'num_samples', 1), domain=args.domain, + upload_notion=getattr(args, 'upload_notion', False), + notion_config=str(args.notion_config) if getattr(args, 'notion_config', None) else None, ) # Single file processing @@ -252,6 +265,39 @@ def run_with_args(args: argparse.Namespace) -> TableState | Dict: payload = _filter_json_safe_state(result, html_paths=[]) print(json.dumps(payload, ensure_ascii=False, indent=2)) + # Upload to Notion if requested (single file) + if getattr(args, 'upload_notion', False): + if not domain: + print("\n⚠️ --upload-notion requires --domain to be specified. Skipping upload.") + else: + qa_results = result.get("qa_results", []) + if qa_results: + print(f"\n{'='*50}") + print(f"Uploading to Notion (domain: {domain})...") + try: + notion_results = [{ + "image_path": str(input_path), + "qa_results": qa_results, + "table_summary": result.get("table_summary"), + "token_usage": result.get("token_usage", 0), + }] + notion_config = str(args.notion_config) if getattr(args, 'notion_config', None) else None + upload_summary = upload_to_notion( + domain=domain, + results=notion_results, + config_path=notion_config, + verbose=True, + provider=args.provider, + ) + print(f"\n✅ Notion upload complete: {upload_summary['success']}/{upload_summary['total']} succeeded") + except ImportError as e: + print(f"\n❌ Notion upload failed: {e}") + print(" Install with: pip install notion-client") + except Exception as e: + print(f"\n❌ Notion upload failed: {e}") + else: + print("\n⚠️ No QA results to upload.") + return result @@ -271,6 +317,8 @@ def run_batch_for_folder( max_k: int = 3, num_samples: int = 1, domain: str | None = None, + upload_notion: bool = False, + notion_config: str | None = None, ) -> Dict[str, any]: """ Execute the flow for all images in a folder (batch processing). @@ -470,6 +518,50 @@ def process_task(task: Dict) -> Dict: print(f"Total: {len(image_files)}, Success: {success_count}, Failed: {failed_count}") print(f"Summary saved to: {summary_file}") + # Upload to Notion if requested + if upload_notion: + if not domain: + print("\n⚠️ --upload-notion requires --domain to be specified. Skipping upload.") + else: + print(f"\n{'='*50}") + print(f"Uploading to Notion (domain: {domain})...") + + # Prepare results for Notion upload + # Load the actual QA results from saved files + notion_results = [] + for result in results: + if result.get("status") in ("success", "partial"): + output_file = result.get("output_file") + if output_file and Path(output_file).exists(): + try: + data = json.loads(Path(output_file).read_text(encoding="utf-8")) + notion_results.append({ + "image_path": data.get("name", result.get("name")), + "qa_results": data.get("qa_results", []), + "table_summary": data.get("table_summary"), + }) + except Exception as e: + print(f"⚠️ Failed to load {output_file}: {e}") + + if notion_results: + try: + upload_summary = upload_to_notion( + domain=domain, + results=notion_results, + config_path=notion_config, + verbose=True, + provider=provider, + ) + summary["notion_upload"] = upload_summary + print(f"\n✅ Notion upload complete: {upload_summary['success']}/{upload_summary['total']} succeeded") + except ImportError as e: + print(f"\n❌ Notion upload failed: {e}") + print(" Install with: pip install notion-client") + except Exception as e: + print(f"\n❌ Notion upload failed: {e}") + else: + print("⚠️ No successful results to upload.") + return summary diff --git a/google_drive_download.py b/google_drive_download.py new file mode 100644 index 0000000..e69de29 diff --git a/notion_import.csv b/notion_import.csv new file mode 100644 index 0000000..3883bdd --- /dev/null +++ b/notion_import.csv @@ -0,0 +1,3 @@ +Name,Image,Question,Answer,Type,Domain +test1,test.png,테스트 질문1,테스트 답변1,lookup,Public +test2,test2.png,테스트 질문2,테스트 답변2,comparison,Public diff --git a/polling_gemini/api_pool.py b/polling_gemini/api_pool.py index 488b9a7..dc4356f 100644 --- a/polling_gemini/api_pool.py +++ b/polling_gemini/api_pool.py @@ -140,6 +140,7 @@ def _is_quota_error(self, error: Exception) -> bool: def generate_content( self, prompt: str, + return_full_response: bool = False, **kwargs ) -> str: """ @@ -147,10 +148,11 @@ def generate_content( Args: prompt: 입력 프롬프트 + return_full_response: True면 (text, usage_metadata) 튜플 반환 **kwargs: GenerativeModel.generate_content에 전달할 추가 인자 Returns: - 생성된 텍스트 + 생성된 텍스트, 또는 (텍스트, usage_metadata) 튜플 Raises: Exception: 모든 API 키로 시도했으나 실패한 경우 @@ -185,6 +187,10 @@ def generate_content( current_key.failed_count = 0 current_key.last_error = None + if return_full_response: + # usage_metadata 추출 + usage_metadata = getattr(response, 'usage_metadata', None) + return response.text, usage_metadata return response.text except google_exceptions.ResourceExhausted as e: diff --git a/polling_gemini/langgraph_integration.py b/polling_gemini/langgraph_integration.py index 1ca3d03..9d4bce3 100644 --- a/polling_gemini/langgraph_integration.py +++ b/polling_gemini/langgraph_integration.py @@ -190,20 +190,37 @@ def _generate( if has_images: # 멀티모달 콘텐츠 - Gemini 형식으로 변환 gemini_content = self._convert_messages_to_gemini_content(messages) - response_text = self.api_pool.generate_content( + response_text, usage_metadata = self.api_pool.generate_content( gemini_content, + return_full_response=True, **generation_params ) else: # 텍스트만 있는 경우 - 단순 프롬프트 prompt = self._convert_messages_to_prompt(messages) - response_text = self.api_pool.generate_content( + response_text, usage_metadata = self.api_pool.generate_content( prompt, + return_full_response=True, **generation_params ) + # 토큰 사용량 메타데이터 구성 + response_metadata = {} + usage_metadata_dict = None + if usage_metadata: + usage_metadata_dict = { + "input_tokens": getattr(usage_metadata, 'prompt_token_count', 0), + "output_tokens": getattr(usage_metadata, 'candidates_token_count', 0), + "total_tokens": getattr(usage_metadata, 'total_token_count', 0), + } + response_metadata["usage"] = usage_metadata_dict + # ChatResult 형식으로 변환 - message = AIMessage(content=response_text) + message = AIMessage( + content=response_text, + response_metadata=response_metadata, + usage_metadata=usage_metadata_dict, + ) generation = ChatGeneration(message=message) return ChatResult(generations=[generation]) diff --git a/pyproject.toml b/pyproject.toml index b6eaa21..d5c0eab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,4 +22,5 @@ dependencies = [ "python-multipart>=0.0.12", "langchain-anthropic>=0.3.0", "pymongo>=4.6.1", + "notion-client>=2.0.0", ] diff --git a/uv.lock b/uv.lock index 2ec4b2b..e72a40e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14'", @@ -991,6 +991,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] +[[package]] +name = "notion-client" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/ff/06695dca5d0691346fbd89ba300a2ceeeaf0b0632fe8235b588e83e505a3/notion_client-2.7.0.tar.gz", hash = "sha256:31bde3ae03ede77650cadce136152916efdd86579ff65f49fc6705fbe462e2fb", size = 26384, upload-time = "2025-10-31T12:10:15.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/6a/9716315432f5aba4c82979f9677aeb101018f0e790835721dc4e01deb933/notion_client-2.7.0-py2.py3-none-any.whl", hash = "sha256:9057a8ac2103ff245556c2a5102bde1d2ccdd3505f66bcc130fc31857731d91e", size = 16999, upload-time = "2025-10-31T12:10:13.835Z" }, +] + [[package]] name = "openai" version = "2.11.0" @@ -1728,6 +1740,7 @@ dependencies = [ { name = "langchain-google-genai" }, { name = "langchain-openai" }, { name = "langgraph" }, + { name = "notion-client" }, { name = "pymongo" }, { name = "pymupdf" }, { name = "python-multipart" }, @@ -1749,6 +1762,7 @@ requires-dist = [ { name = "langchain-google-genai", specifier = ">=4.0.0" }, { name = "langchain-openai", specifier = ">=1.1.0" }, { name = "langgraph", specifier = ">=0.0.30" }, + { name = "notion-client", specifier = ">=2.0.0" }, { name = "pymongo", specifier = ">=4.6.1" }, { name = "pymupdf", specifier = ">=1.26.7" }, { name = "python-multipart", specifier = ">=0.0.12" },