diff --git a/usecase/solar-knowledge-management/.env.example b/usecase/solar-knowledge-management/.env.example
new file mode 100644
index 0000000..ea786ed
--- /dev/null
+++ b/usecase/solar-knowledge-management/.env.example
@@ -0,0 +1,32 @@
+# .env.example -> .env
+
+# ===============================================
+# API Key 설정 (공통 요소)
+# ===============================================
+UPSTAGE_API_KEY=your_api_key_here
+TAVILY_API_KEY=your_api_key_here
+
+
+
+
+# ===============================================
+# 노트 분할 관련 요소
+# ===============================================
+UPSTAGE_API_BASE=https://api.upstage.ai/v1/solar
+MODEL_NAME=solar-pro2
+
+# Application Settings
+DEFAULT_WORKSPACE=./workspace
+PROMPTS_DIR=./prompts
+
+# Atomic Note Generation
+DEFAULT_ATOM_DIRECTION= this is for Zettelkasten like atomic note.
+
+# LLM Settings
+DEFAULT_TEMPERATURE=0.7
+DEFAULT_MAX_TOKENS=8192
+HTTP_TIMEOUT_ASYNC=120.0
+HTTP_TIMEOUT_SYNC=120.0
+
+# File Settings
+ATOMIC_NOTES_SUFFIX=-atoms
diff --git a/usecase/solar-knowledge-management/.gitignore b/usecase/solar-knowledge-management/.gitignore
new file mode 100644
index 0000000..8160ada
--- /dev/null
+++ b/usecase/solar-knowledge-management/.gitignore
@@ -0,0 +1,334 @@
+# Created by https://www.toptal.com/developers/gitignore/api/python,jupyternotebooks,vim,macos,windows,venv,linux,dotenv,git,visualstudiocode
+# Edit at https://www.toptal.com/developers/gitignore?templates=python,jupyternotebooks,vim,macos,windows,venv,linux,dotenv,git,visualstudiocode
+
+notebooks/
+archive/
+backend/related_note/vector_store/
+backend/related_note/embedded_markers.txt
+
+### dotenv ###
+.env
+
+### Git ###
+# Created by git for backups. To disable backups in Git:
+# $ git config --global mergetool.keepBackup false
+*.orig
+
+# Created by git when using merge tools for conflicts
+*.BACKUP.*
+*.BASE.*
+*.LOCAL.*
+*.REMOTE.*
+*_BACKUP_*.txt
+*_BASE_*.txt
+*_LOCAL_*.txt
+*_REMOTE_*.txt
+
+### JupyterNotebooks ###
+# gitignore template for Jupyter Notebooks
+# website: http://jupyter.org/
+
+.ipynb_checkpoints
+*/.ipynb_checkpoints/*
+
+# IPython
+profile_default/
+ipython_config.py
+
+# Remove previous ipynb_checkpoints
+# git rm -r .ipynb_checkpoints/
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+
+# IPython
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+### Python Patch ###
+# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
+poetry.toml
+
+# ruff
+.ruff_cache/
+
+# LSP config files
+pyrightconfig.json
+
+### venv ###
+# Virtualenv
+# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
+[Bb]in
+[Ii]nclude
+[Ll]ib
+[Ll]ib64
+[Ll]ocal
+[Ss]cripts
+pyvenv.cfg
+pip-selfcheck.json
+
+### Vim ###
+# Swap
+[._]*.s[a-v][a-z]
+!*.svg # comment out if you don't need vector files
+[._]*.sw[a-p]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
+[._]sw[a-p]
+
+# Session
+Session.vim
+Sessionx.vim
+
+# Temporary
+.netrwhist
+# Auto-generated tag files
+tags
+# Persistent undo
+[._]*.un~
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+.ionide
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/python,jupyternotebooks,vim,macos,windows,venv,linux,dotenv,git,visualstudiocode
\ No newline at end of file
diff --git a/usecase/solar-knowledge-management/.python-version b/usecase/solar-knowledge-management/.python-version
new file mode 100644
index 0000000..24ee5b1
--- /dev/null
+++ b/usecase/solar-knowledge-management/.python-version
@@ -0,0 +1 @@
+3.13
diff --git a/usecase/solar-knowledge-management/.streamlit/config.toml b/usecase/solar-knowledge-management/.streamlit/config.toml
new file mode 100644
index 0000000..32df682
--- /dev/null
+++ b/usecase/solar-knowledge-management/.streamlit/config.toml
@@ -0,0 +1,2 @@
+[theme]
+base="light"
diff --git a/usecase/solar-knowledge-management/LICENSE b/usecase/solar-knowledge-management/LICENSE
new file mode 100644
index 0000000..0d8ed05
--- /dev/null
+++ b/usecase/solar-knowledge-management/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Jaemin Hong
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/usecase/solar-knowledge-management/README-KO.md b/usecase/solar-knowledge-management/README-KO.md
new file mode 100644
index 0000000..70ac09e
--- /dev/null
+++ b/usecase/solar-knowledge-management/README-KO.md
@@ -0,0 +1,276 @@
+# UpThink
+
+**[Upstage AI Ambassador]** Personal Knowledge Management with Upstage Solar Pro 2 ✨
+
+## Overview
+
+UpThink는 개인 지식 관리 환경(Obsidian)에서 발생하는 반복적인 수작업 비용을 최소화하는 서비스입니다. \
+지식을 정리하는 과정에서 필연적으로 발생하는 다음의 병목 현상들을 해결합니다.
+
+| 문제 | 설명 |
+|------|------|
+| **이미지 데이터 처리** | 시각 정보를 텍스트로 변환하는 수동 작업 |
+| **태그 관리** | 태그 컨벤션 유지 및 스타일링 고민 |
+| **지식 연결성 부재** | 연관된 과거 노트를 찾기 위한 탐색 비용 |
+| **비구조화된 문서** | 방대한 노트 분할의 필요성 |
+
+UpThink는 **Upstage Solar Pro 2**의 강력한 언어 이해 능력을 기반으로 이러한 과정을 자동화합니다. \
+사용자는 단순 반복 작업에서 벗어나, 가장 중요한 사고 활동에만 몰입할 수 있습니다.
+
+### Table of Contents
+
+- [Overview](#overview)
+- [Key Features](#key-features)
+- [Tech Stack](#tech-stack)
+- [Architecture](#architecture)
+ - [Flow Chart](#flow-chart)
+- [Installation](#installation)
+- [Usage](#usage)
+ - [시연 영상 보러가기](#-시연-영상-보러가기-youtube-)
+- [Project Structure](#project-structure)
+- [Members & Roles](#members--roles)
+- [Acknowledgements](#acknowledgements)
+
+## Key Features
+
+### 1️⃣ 이미지 대체 텍스트 생성
+
+노트 내 이미지에서 텍스트를 추출하고, 이미지 내용을 설명하는 대체 텍스트를 자동으로 생성합니다.
+
+- **Upstage Document Parse**로 이미지에서 OCR 및 문서 구조 추출
+- **Solar Pro 2**로 추출된 텍스트를 바탕으로 50단어 내외의 대체 텍스트 생성
+- 마크다운 파일 내 모든 이미지를 일괄 처리
+- 이미지 링크 `![[image.png]]` 형식의 바로 아래에 자동 삽입
+
+### 2️⃣ 태그 추천
+
+노트 내용을 분석하여 적절한 태그를 추천하고, 기존 Vault의 태그 컨벤션과 일관성을 유지합니다.
+
+- Vault 내 기존 태그 자동 수집 (해시태그 `#tag` 및 YAML frontmatter 지원)
+- 사용자 정의 태그 가이드라인 설정 (언어, 대소문자, 구분자, 태그 개수)
+- **Solar Pro 2**로 노트 내용 기반 태그 생성
+- **Qwen Embedding** 모델로 기존 태그와 유사도 비교 및 매칭
+- YAML frontmatter 형식으로 태그 자동 삽입
+
+### 3️⃣ 연관 노트 추천
+
+현재 노트와 의미적으로 유사한 노트를 찾아 자동으로 연결합니다.
+
+- **Upstage Embedding Model**과 **Chroma DB**로 Vault 내 노트 벡터화
+- 임베딩되지 않은 노트 자동 식별 및 일괄 처리
+- 긴 노트도 청킹하여 처리 가능
+- 유사도 검색으로 Top 3 연관 노트 추천
+- 백링크 `[[note]]` 형식으로 `## Related Notes` 섹션에 자동 삽입
+
+### 4️⃣ 노트 분할
+
+방대한 노트를 주제별로 분리하여 원자화하고 상호 연결된 지식 체계를 구축합니다.
+
+- **Solar Pro 2**로 노트 내 주제(Topic) 자동 추출
+- 템플릿 기반의 유연한 분할 전략 지원
+- 추출된 주제 편집, 삭제, 추가 가능
+- 분할된 원자 노트 자동 생성 및 저장
+- 원본 노트에 백링크 및 `## Generated Atomic Notes` 섹션 자동 삽입
+
+## Tech Stack
+
+| 분류 | 기술 |
+|------|------|
+| **Language** | Python 3.13 |
+| **Frontend** | Streamlit |
+| **LLM** | Upstage Solar Pro 2 |
+| **Document AI** | Upstage Document Parse |
+| **Embedding** | Upstage Embedding, Qwen3-Embedding-0.6B |
+| **Vector DB** | Chroma DB |
+| **Framework** | LangChain |
+| **Package Manager** | uv |
+
+## Architecture
+
+
+
+
+
+| 레이어 | 구성 요소 | 설명 |
+|--------|-----------|------|
+| **Frontend** | Streamlit | 웹 기반 사용자 인터페이스 |
+| **Backend** | Python 모듈 | 4가지 핵심 기능 구현 |
+| **Upstage API** | Solar Pro 2, Document Parse, Embedding Model | LLM, OCR, 벡터 임베딩 |
+| **Local** | Qwen Embedding, Chroma DB | 태그 유사도 비교, 노트 벡터 저장 |
+
+### Flow Chart
+
+#### 이미지 대체 텍스트 생성
+
+
+
+
+
+| Step | Flow | Backend 주요 모듈 |
+|:----:|------|-------------------|
+| 1 | 이미지 링크 추출 및 대체 텍스트 존재 여부 확인 | `MarkdownImageProcessor._collect_images_to_process()` |
+| 1 | Vault 내 이미지 파일 경로 탐색 | `MarkdownImageProcessor._find_image_in_vault()` |
+| 2 | 이미지에서 텍스트 추출 | `OCRProcessor.extract_text()` |
+| 2 | 대체 텍스트 생성 | `AltTextGenerator.generate_alt_text()` |
+| 3 | 이미지 링크 하단에 대체 텍스트 삽입 | `MarkdownImageProcessor.process_images()` |
+
+#### 태그 추천
+
+
+
+
+
+| Step | Flow | Backend 주요 모듈 |
+|:----:|------|-------------------|
+| 1 | 기존 태그 수집 및 확인 | `TagExtractor.get_unique_tags()`, `TagExtractor.count_tags()` |
+| 2 | 태그 가이드라인 설정 및 신규 태그 생성 | `GuidelineGenerator()`, `TagGenerator.generate_tags()` |
+| 3 | 기존 태그와 신규 태그 비교 | `TagComparator.compare_tags()` |
+| 3 | 최종 태그 제안 | `TagComparator.get_final_tags()` |
+| 4 | YAML Frontmatter 삽입 | `add_yaml_frontmatter()` |
+
+#### 연관 노트 추천
+
+
+
+
+
+| Step | Flow | Backend 주요 모듈 |
+|:----:|------|-------------------|
+| 1 | 임베딩되지 않은 노트 확인 | `Related_Note.get_unembedded_notes()` |
+| 2 | 전처리 및 청킹 | `Related_Note.clean_text()`, `Related_Note.chunk_text()` |
+| 2 | 노트 임베딩 및 DB 저장 | `Related_Note.index_unembedded_notes()` |
+| 3 | 연관 노트 검색 | `Related_Note.find_related_notes()` |
+| 4 | 백링크 삽입 | `Related_Note.append_related_links()` |
+
+#### 노트 분할
+
+
+
+
+
+| Step | Flow | Backend 주요 모듈 |
+|:----:|------|-------------------|
+| 1 | 프롬프트 템플릿 로드 | `PromptLoader.load_template()` |
+| 1 | Topic 추출 | `UpstageClient.generate_with_template_sync()` |
+| 2 | Topic 목록 파싱 | `ResponseParser.parse_topics_from_json()` |
+| 3 | 원자 노트 생성 | `FileHandler.create_atomic_note()` |
+| 3 | 백링크 삽입 | `FileHandler.insert_backlinks()` |
+
+## Installation
+
+### 지원 환경
+- macOS
+- Windows (PowerShell, CMD)
+
+### uv 설치
+
+- https://docs.astral.sh/uv/getting-started/installation/
+
+#### Homebrew
+
+```
+brew install uv
+```
+
+#### Windows
+
+```
+powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
+```
+
+### 프로젝트 설정
+
+```
+# git clone
+git clone https://github.com/geminii01/product-usecase-knowledge-management-upthink.git
+cd product-usecase-knowledge-management-upthink
+```
+```
+# 환경 변수 설정 (필수!)
+cp .env.example .env
+
+# .env 파일을 열어서 API 키 입력
+# UPSTAGE_API_KEY=your_api_key_here
+# TAVILY_API_KEY=your_api_key_here
+```
+```
+# Python 3.13과 의존성 자동 설치
+uv sync
+```
+
+### 실행
+
+```
+streamlit run frontend/app.py
+
+# 아래 Local URL로 접속!
+# http://localhost:8501
+```
+
+## Usage
+
+### 🎬 시연 영상 보러가기: [YouTube](https://www.youtube.com/watch?v=8bjLew7KTW4) 🎬
+
+### 기본 사용법
+
+1. 사이드바에서 **Vault 경로**를 입력합니다. (Obsidian Vault의 절대 경로)
+2. 처리할 **Markdown 파일**을 업로드합니다.
+3. 원하는 기능 페이지로 이동하여 실행합니다.
+
+## Project Structure
+
+```
+upthink/
+├── frontend/ # Streamlit 프론트엔드
+│ ├── app.py # 메인 앱 (라우팅, 공통 사이드바)
+│ ├── home.py # 홈 페이지
+│ ├── image_ocr.py # 이미지 대체 텍스트 생성 UI
+│ ├── tag_suggest.py # 태그 추천 UI
+│ ├── related_note.py # 연관 노트 추천 UI
+│ ├── note_split.py # 노트 분할 UI
+│ └── note_freshness.py # 최신 정보 확인 UI
+│
+├── backend/ # 백엔드 로직
+│ ├── image_ocr/ # 이미지 OCR 및 대체 텍스트 생성
+│ │ ├── ocr_processor.py # Document Parse API 연동
+│ │ ├── alt_text_generator.py # Solar Pro 2 대체 텍스트 생성
+│ │ └── markdown_processor.py # 마크다운 이미지 처리
+│ │
+│ ├── tag_suggest/ # 태그 추천
+│ │ ├── tag_extractor.py # 태그 패턴 2가지 추출
+│ │ ├── tag_guidelines.py # 가이드라인 생성
+│ │ ├── tag_generator.py # Solar Pro 2 태그 생성
+│ │ ├── tag_comparator.py # Qwen Embedding 유사도 비교
+│ │ └── markdown_processor.py # YAML frontmatter 처리
+│ │
+│ ├── related_note/ # 연관 노트 추천
+│ │ └── related_note.py # Chroma DB 기반 유사도 검색
+│ │
+│ ├── note_split/ # 노트 분할
+│ │ ├── config.py # 설정
+│ │ ├── models.py # 데이터 모델
+│ │ ├── core/ # 상태 관리, 파일 처리
+│ │ ├── llm/ # LLM 클라이언트, 프롬프트 로더
+│ │ └── ui/ # UI 컴포넌트
+│ │
+│ └── note_freshness/ # 최신성 검증
+│ ├── api/ # Tavily, Wikipedia API
+│ ├── core/ # 상태 관리
+│ └── llm/ # LLM 연동
+│
+├── prompts/ # 프롬프트 템플릿 (YAML)
+├── pyproject.toml # 프로젝트 설정 및 의존성
+└── .env.example # 환경 변수 예시
+```
+
+## Members & Roles
+
+| 김수연 | 오주영 | 윤이지 | 홍재민 |
+|:------:|:------:|:------:|:------:|
+| 
| 
| 
| 
|
+| ▪︎ 이미지 대체 텍스트 생성 기능 개발 | ▪︎ 노트 분할 기능 개발
▪︎ 최신성 검증 통합 | ▪︎ PM
▪︎ 연관 노트 추천 기능 개발 | ▪︎ 태그 추천 기능 개발
▪︎ GitHub 관리 & 팀 코드 통합 |
+
+## Acknowledgements
+
+이 프로젝트는 **Upstage AI Ambassador** 활동의 일환으로 진행되었습니다. \
+프로젝트를 진행할 수 있도록 Credit을 지원해 주신 **[Upstage](https://www.upstage.ai/)** 에 감사드립니다.
diff --git a/usecase/solar-knowledge-management/README.md b/usecase/solar-knowledge-management/README.md
new file mode 100644
index 0000000..0747989
--- /dev/null
+++ b/usecase/solar-knowledge-management/README.md
@@ -0,0 +1,276 @@
+# UpThink
+
+**[Upstage AI Ambassador]** Personal Knowledge Management with Upstage Solar Pro 2 ✨
+
+## Overview
+
+UpThink is a service designed to minimize the repetitive manual effort in Personal Knowledge Management environments (specifically Obsidian).
+It addresses the following bottlenecks that inevitably arise during the knowledge organization process.
+
+| Problem | Description |
+|------|------|
+| **Image Data Processing** | Manual conversion of visual information into text |
+| **Tag Management** | Maintaining tag conventions and styling concerns |
+| **Lack of Knowledge Connectivity** | Search costs for finding relevant past notes |
+| **Unstructured Documents** | Need for splitting massive notes |
+
+UpThink automates these processes based on the powerful language understanding capabilities of **Upstage Solar Pro 2**.
+Users can break free from simple repetitive tasks and focus on what matters most—thinking.
+
+### Table of Contents
+
+- [Overview](#overview)
+- [Key Features](#key-features)
+- [Tech Stack](#tech-stack)
+- [Architecture](#architecture)
+ - [Flow Chart](#flow-chart)
+- [Installation](#installation)
+- [Usage](#usage)
+ - [Watch Demo Video](#-watch-demo-video-youtube-)
+- [Project Structure](#project-structure)
+- [Members & Roles](#members--roles)
+- [Acknowledgements](#acknowledgements)
+
+## Key Features
+
+### 1️⃣ Image Alt Text Generation
+
+Extracts text from images within notes and automatically generates alt text describing the image content.
+
+- Perform OCR and extract document structure from images using **Upstage Document Parse**
+- Generate alt text of around 50 words based on extracted text using **Solar Pro 2**
+- Batch process all images within markdown files
+- Automatically insert alt text below image links in `![[image.png]]` format
+
+### 2️⃣ Tag Recommendation
+
+Analyzes note content to recommend appropriate tags and maintains consistency with existing Vault tag conventions.
+
+- Automatically collect existing tags in Vault (supports hashtags `#tag` and YAML frontmatter)
+- Set user-defined tag guidelines (language, case, separators, number of tags)
+- Generate tags based on note content using **Solar Pro 2**
+- Compare and match similarity with existing tags using **Qwen Embedding** model
+- Automatically insert tags in YAML frontmatter format
+
+### 3️⃣ Related Note Recommendation
+
+Finds notes semantically similar to the current note and automatically connects them.
+
+- Vectorize notes in Vault using **Upstage Embedding Model** and **Chroma DB**
+- Automatically identify and batch process unembedded notes
+- Process long notes through chunking
+- Recommend Top 3 related notes via similarity search
+- Automatically append backlinks to the `## Related Notes` section using the `[[note]]` format
+
+### 4️⃣ Note Splitting
+
+Splits massive notes by topic to atomize them and build an interconnected knowledge system.
+
+- Automatically extract topics within notes using **Solar Pro 2**
+- Support flexible splitting strategies based on templates
+- Edit, delete, or add extracted topics
+- Automatically generate and save split atomic notes
+- Automatically insert backlinks and `## Generated Atomic Notes` section in original note
+
+## Tech Stack
+
+| Category | Technology |
+|------|------|
+| **Language** | Python 3.13 |
+| **Frontend** | Streamlit |
+| **LLM** | Upstage Solar Pro 2 |
+| **Document AI** | Upstage Document Parse |
+| **Embedding** | Upstage Embedding, Qwen3-Embedding-0.6B |
+| **Vector DB** | Chroma DB |
+| **Framework** | LangChain |
+| **Package Manager** | uv |
+
+## Architecture
+
+
+
+
+
+| Layer | Component | Description |
+|--------|-----------|------|
+| **Frontend** | Streamlit | Web-based User Interface |
+| **Backend** | Python Modules | Implementation of 4 core features |
+| **Upstage API** | Solar Pro 2, Document Parse, Embedding Model | LLM, OCR, Vector Embedding |
+| **Local** | Qwen Embedding, Chroma DB | Tag similarity comparison, Note vector storage |
+
+### Flow Chart
+
+#### Image Alt Text Generation
+
+
+
+
+
+| Step | Flow | Key Backend Modules |
+|:----:|------|-------------------|
+| 1 | Extract image links and check alt text existence | `MarkdownImageProcessor._collect_images_to_process()` |
+| 1 | Search image file paths in Vault | `MarkdownImageProcessor._find_image_in_vault()` |
+| 2 | Extract text from image | `OCRProcessor.extract_text()` |
+| 2 | Generate alt text | `AltTextGenerator.generate_alt_text()` |
+| 3 | Insert alt text below image link | `MarkdownImageProcessor.process_images()` |
+
+#### Tag Recommendation
+
+
+
+
+
+| Step | Flow | Key Backend Modules |
+|:----:|------|-------------------|
+| 1 | Collect and check existing tags | `TagExtractor.get_unique_tags()`, `TagExtractor.count_tags()` |
+| 2 | Set tag guidelines and generate new tags | `GuidelineGenerator()`, `TagGenerator.generate_tags()` |
+| 3 | Compare existing and new tags | `TagComparator.compare_tags()` |
+| 3 | Suggest final tags | `TagComparator.get_final_tags()` |
+| 4 | Insert YAML Frontmatter | `add_yaml_frontmatter()` |
+
+#### Related Note Recommendation
+
+
+
+
+
+| Step | Flow | Key Backend Modules |
+|:----:|------|-------------------|
+| 1 | Identify unembedded notes | `Related_Note.get_unembedded_notes()` |
+| 2 | Preprocessing and chunking | `Related_Note.clean_text()`, `Related_Note.chunk_text()` |
+| 2 | Embed notes and save to DB | `Related_Note.index_unembedded_notes()` |
+| 3 | Search related notes | `Related_Note.find_related_notes()` |
+| 4 | Insert backlinks | `Related_Note.append_related_links()` |
+
+#### Note Splitting
+
+
+
+
+
+| Step | Flow | Key Backend Modules |
+|:----:|------|-------------------|
+| 1 | Load prompt template | `PromptLoader.load_template()` |
+| 1 | Extract Topic | `UpstageClient.generate_with_template_sync()` |
+| 2 | Parse Topic list | `ResponseParser.parse_topics_from_json()` |
+| 3 | Generate atomic notes | `FileHandler.create_atomic_note()` |
+| 3 | Insert backlinks | `FileHandler.insert_backlinks()` |
+
+## Installation
+
+### Supported Environments
+- macOS
+- Windows (PowerShell, CMD)
+
+### Install uv
+
+- https://docs.astral.sh/uv/getting-started/installation/
+
+#### Homebrew
+
+```
+brew install uv
+```
+
+#### Windows
+
+```
+powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
+```
+
+### Project Setup
+
+```
+# git clone
+git clone https://github.com/geminii01/product-usecase-knowledge-management-upthink.git
+cd product-usecase-knowledge-management-upthink
+```
+```
+# Environment Variable Setup (Required!)
+cp .env.example .env
+
+# Open .env file and enter API keys
+# UPSTAGE_API_KEY=your_api_key_here
+# TAVILY_API_KEY=your_api_key_here
+```
+```
+# Install Python 3.13 and dependencies automatically
+uv sync
+```
+
+### Run
+
+```
+streamlit run frontend/app.py
+
+# Access via Local URL below!
+# http://localhost:8501
+```
+
+## Usage
+
+### 🎬 Watch Demo Video: [YouTube](https://www.youtube.com/watch?v=8bjLew7KTW4) 🎬
+
+### Basic Usage
+
+1. Enter **Vault Path** in the sidebar. (Absolute path to Obsidian Vault)
+2. Upload **Markdown file** to process.
+3. Go to the desired feature page and execute.
+
+## Project Structure
+
+```
+upthink/
+├── frontend/ # Streamlit Frontend
+│ ├── app.py # Main App (Routing, Common Sidebar)
+│ ├── home.py # Home Page
+│ ├── image_ocr.py # Image Alt Text Generation UI
+│ ├── tag_suggest.py # Tag Recommendation UI
+│ ├── related_note.py # Related Note Recommendation UI
+│ ├── note_split.py # Note Splitting UI
+│ └── note_freshness.py # Freshness Check UI
+│
+├── backend/ # Backend Logic
+│ ├── image_ocr/ # Image OCR & Alt Text Generation
+│ │ ├── ocr_processor.py # Document Parse API Integration
+│ │ ├── alt_text_generator.py # Solar Pro 2 Alt Text Generation
+│ │ └── markdown_processor.py # Markdown Image Processing
+│ │
+│ ├── tag_suggest/ # Tag Suggestion
+│ │ ├── tag_extractor.py # Extract 2 Tag Patterns
+│ │ ├── tag_guidelines.py # Guideline Generation
+│ │ ├── tag_generator.py # Solar Pro 2 Tag Generation
+│ │ ├── tag_comparator.py # Qwen Embedding Similarity Comparison
+│ │ └── markdown_processor.py # YAML frontmatter Processing
+│ │
+│ ├── related_note/ # Related Note Recommendation
+│ │ └── related_note.py # Chroma DB based Similarity Search
+│ │
+│ ├── note_split/ # Note Splitting
+│ │ ├── config.py # Config
+│ │ ├── models.py # Data Models
+│ │ ├── core/ # State Management, File Handling
+│ │ ├── llm/ # LLM Client, Prompt Loader
+│ │ └── ui/ # UI Components
+│ │
+│ └── note_freshness/ # Freshness Verification
+│ ├── api/ # Tavily, Wikipedia API
+│ ├── core/ # State Management
+│ └── llm/ # LLM Integration
+│
+├── prompts/ # Prompt Templates (YAML)
+├── pyproject.toml # Project Configuration & Dependencies
+└── .env.example # Environment Variable Example
+```
+
+## Members & Roles
+
+| Kim Su-yeon | Oh Ju-yeong | Yoon I-ji | Hong Jae-min |
+|:------:|:------:|:------:|:------:|
+| 
| 
| 
| 
|
+| ▪︎ Developed Image Alt Text Generation Feature | ▪︎ Developed Note Splitting Feature
▪︎ Integrated Freshness Verification | ▪︎ PM
▪︎ Developed Related Note Recommendation Feature | ▪︎ Developed Tag Recommendation Feature
▪︎ GitHub Management & Team Code Integration |
+
+## Acknowledgements
+
+This project was conducted as part of the **Upstage AI Ambassador** program. \
+We thank **[Upstage](https://www.upstage.ai/)** for providing credits to support this project.
diff --git a/usecase/solar-knowledge-management/backend/__init__.py b/usecase/solar-knowledge-management/backend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/usecase/solar-knowledge-management/backend/image_ocr/__init__.py b/usecase/solar-knowledge-management/backend/image_ocr/__init__.py
new file mode 100644
index 0000000..7769530
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/image_ocr/__init__.py
@@ -0,0 +1,9 @@
+from .ocr_processor import OCRProcessor
+from .alt_text_generator import AltTextGenerator
+from .markdown_processor import MarkdownImageProcessor
+
+__all__ = [
+ "OCRProcessor",
+ "AltTextGenerator",
+ "MarkdownImageProcessor",
+]
diff --git a/usecase/solar-knowledge-management/backend/image_ocr/alt_text_generator.py b/usecase/solar-knowledge-management/backend/image_ocr/alt_text_generator.py
new file mode 100644
index 0000000..1dde161
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/image_ocr/alt_text_generator.py
@@ -0,0 +1,105 @@
+"""
+LLM을 사용하여 OCR 텍스트로부터 대체 텍스트 생성
+"""
+
+import os
+from openai import OpenAI
+from openai import APIError as OpenAIAPIError
+from dotenv import load_dotenv
+
+load_dotenv()
+
+
+class AltTextGenerator:
+ """OCR 추출 텍스트를 solar-pro2 LLM에 입력하여 대체 텍스트를 생성"""
+
+ def __init__(
+ self,
+ model: str = "solar-pro2",
+ max_tokens: int = 150,
+ ):
+ """
+ AltTextGenerator 초기화
+
+ Args:
+ model: 사용할 LLM 모델 (기본값: "solar-pro2")
+ max_tokens: 최대 토큰 수 (기본값: 150)
+
+ Raises:
+ ValueError: UPSTAGE_API_KEY 환경 변수가 설정되지 않은 경우
+ """
+ api_key = os.getenv("UPSTAGE_API_KEY")
+ if not api_key:
+ raise ValueError("UPSTAGE_API_KEY 환경 변수가 설정되지 않았습니다")
+
+ self.client = OpenAI(
+ api_key=api_key,
+ base_url="https://api.upstage.ai/v1",
+ )
+ self.model = model
+ self.max_tokens = max_tokens
+
+ def generate_alt_text(self, extracted_text: str) -> str:
+ """
+ 추출된 텍스트를 solar-pro2 LLM에 입력하여 대체 텍스트를 생성합니다.
+
+ Args:
+ extracted_text: OCR로 추출된 텍스트
+
+ Returns:
+ 생성된 대체 텍스트 (오류 시 ERROR 메시지)
+
+ Raises:
+ OpenAIAPIError: LLM API 호출 오류
+ Exception: 기타 처리 오류
+ """
+ if not extracted_text:
+ error_msg = "ERROR: OCR에서 추출된 텍스트가 비어있습니다. 이미지에 텍스트가 없거나 OCR이 실패했을 수 있습니다."
+ print(f"[WARNING] {error_msg}")
+ return error_msg
+
+ if extracted_text.startswith("ERROR"):
+ return extracted_text
+
+ try:
+ # LLM 프롬프트
+ prompt_text: str = (
+ f"다음은 이미지에서 추출된 텍스트와 문서 구조 분석 결과입니다.\n\n---\n{extracted_text}\n---\n\n"
+ f"이 내용을 바탕으로 이미지의 내용을 상세히 설명하고, 이 설명을 시각 장애인을 위한 **대체 텍스트**로 "
+ f"50단어 내외의 한국어로 생성해주세요. 오직 생성된 대체 텍스트만 출력하세요."
+ )
+
+ # solar-pro2 LLM 호출 (텍스트 전용)
+ response = self.client.chat.completions.create(
+ model=self.model,
+ messages=[{"role": "user", "content": prompt_text}],
+ max_tokens=self.max_tokens,
+ )
+
+ return response.choices[0].message.content.strip()
+
+ except OpenAIAPIError as e:
+ error_msg = f"ERROR: LLM 추론 API 오류 (HTTP {e.response.status_code})"
+ print(f"[ERROR] {error_msg}")
+ print(f"[ERROR] 응답 상세: {e.response.text}")
+ return f"{error_msg}: {e.response.text[:100]}"
+ except Exception as e:
+ error_msg = f"ERROR: LLM Processing Failed - {str(e)}"
+ print(f"[ERROR] {error_msg}")
+ return error_msg
+
+
+if __name__ == "__main__":
+ # 테스트 코드
+ generator = AltTextGenerator()
+
+ # 테스트용 OCR 추출 텍스트
+ test_extracted_text = """
+ Machine Learning 개요
+ - 지도 학습: 레이블이 있는 데이터로 학습
+ - 비지도 학습: 레이블이 없는 데이터로 패턴 발견
+ - 강화 학습: 보상을 통한 학습
+ """
+
+ result = generator.generate_alt_text(test_extracted_text)
+ print(f"[INFO] 생성된 대체 텍스트: {result}")
diff --git a/usecase/solar-knowledge-management/backend/image_ocr/markdown_processor.py b/usecase/solar-knowledge-management/backend/image_ocr/markdown_processor.py
new file mode 100644
index 0000000..f475122
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/image_ocr/markdown_processor.py
@@ -0,0 +1,215 @@
+"""
+마크다운 파일 내 이미지 처리 모듈
+"""
+
+import re
+from pathlib import Path
+from typing import List, Dict, Callable, Optional
+
+from .ocr_processor import OCRProcessor
+from .alt_text_generator import AltTextGenerator
+
+
+class MarkdownImageProcessor:
+ """마크다운 파일에서 이미지를 찾아 대체 텍스트를 생성하고 업데이트 (Obsidian 전용)"""
+
+ # Obsidian 위키링크 형식: ![[image.png]]
+ OBSIDIAN_IMAGE_PATTERN = re.compile(
+ r"!\[\[(?P[^\]]+\.(png|jpg|jpeg))\]\]", re.IGNORECASE
+ )
+
+ def __init__(self):
+ """MarkdownImageProcessor 초기화"""
+ self.ocr_processor = OCRProcessor()
+ self.alt_text_generator = AltTextGenerator()
+ self._image_cache = {} # 이미지 파일명 -> 경로 캐시
+
+ def _build_image_cache(self, vault_root: Path) -> None:
+ """
+ Vault 전체에서 이미지 파일을 재귀적으로 검색하여 캐시 생성
+
+ Args:
+ vault_root: Vault 루트 경로
+ """
+ self._image_cache = {}
+
+ # 지원하는 이미지 확장자
+ image_extensions = {".png", ".jpg", ".jpeg"}
+
+ # Vault 전체를 재귀적으로 검색
+ for image_path in vault_root.rglob("*"):
+ if image_path.is_file() and image_path.suffix.lower() in image_extensions:
+ filename = image_path.name
+ # 같은 파일명이 여러 개 있을 경우, 첫 번째 것을 사용
+ if filename not in self._image_cache:
+ self._image_cache[filename] = image_path
+
+ print(
+ f"[INFO] Vault에서 {len(self._image_cache)}개의 이미지 파일을 찾았습니다."
+ )
+
+ def _find_image_in_vault(self, filename: str, vault_root: Path) -> Optional[Path]:
+ """
+ Vault 전체에서 파일명으로 이미지 찾기
+
+ Args:
+ filename: 이미지 파일명
+ vault_root: Vault 루트 경로
+
+ Returns:
+ 이미지 파일 경로 (없으면 None)
+ """
+ # 캐시가 비어있으면 캐시 생성
+ if not self._image_cache:
+ self._build_image_cache(vault_root)
+
+ return self._image_cache.get(filename)
+
+ def process_images(
+ self,
+ md_content: str,
+ vault_root: Path,
+ progress_callback: Optional[Callable[[int, int, str], None]] = None,
+ ) -> tuple[str, List[Dict]]:
+ """
+ 마크다운 내용에서 이미지를 찾아 대체 텍스트를 생성하고 업데이트
+
+ Args:
+ md_content: 원본 마크다운 내용
+ vault_root: Vault 루트 경로 (이미지 파일을 찾을 기준 폴더)
+ progress_callback: 진행 상황 콜백 함수 (현재 인덱스, 전체 개수, 이미지 경로)
+
+ Returns:
+ (업데이트된 마크다운 내용, 처리된 이미지 정보 리스트)
+ """
+ # 1. 대체 텍스트가 없는 이미지 수집
+ images_to_process = self._collect_images_to_process(md_content, vault_root)
+
+ if not images_to_process:
+ return md_content, []
+
+ # 2. 각 이미지에 대해 OCR + LLM 대체 텍스트 생성
+ new_content = md_content
+ offset = 0
+ processed_images = []
+
+ for i, img_data in enumerate(images_to_process):
+ # 진행 상황 콜백 호출
+ if progress_callback:
+ progress_callback(i + 1, len(images_to_process), img_data["src"])
+
+ # 1단계: OCR/Parse 텍스트 추출
+ extracted_text = self.ocr_processor.extract_text(img_data["path"])
+
+ # 2단계: LLM 추론으로 대체 텍스트 생성
+ if "ERROR" in extracted_text:
+ new_alt_text = extracted_text
+ else:
+ new_alt_text = self.alt_text_generator.generate_alt_text(extracted_text)
+
+ # 텍스트 삽입 로직: 이미지 링크 아래에 대체 텍스트 추가
+ match = img_data["match_object"]
+ original_string = match.group(0)
+
+ # Obsidian 위키링크 형식 유지 + 아래에 코드 블록으로 대체 텍스트 추가
+ replacement_string = f"{original_string}\n```\n(대체 텍스트 by Upstage)\n{new_alt_text}\n```\n"
+
+ start = match.start() + offset
+ end = match.end() + offset
+
+ new_content = new_content[:start] + replacement_string + new_content[end:]
+ offset += len(replacement_string) - len(original_string)
+
+ # 처리된 이미지 정보 저장
+ processed_images.append(
+ {
+ "src": img_data["src"],
+ "new_alt_text": new_alt_text,
+ }
+ )
+
+ return new_content, processed_images
+
+ def _collect_images_to_process(
+ self, md_content: str, vault_root: Path
+ ) -> List[Dict]:
+ """
+ 마크다운 내용에서 Obsidian 위키링크 형식의 이미지를 수집
+
+ Args:
+ md_content: 마크다운 내용
+ vault_root: Vault 루트 경로
+
+ Returns:
+ 처리할 이미지 정보 리스트
+ """
+ images_to_process = []
+
+ # Obsidian 위키링크 형식 매칭: ![[image.png]]
+ for match in self.OBSIDIAN_IMAGE_PATTERN.finditer(md_content):
+ filename = match.group("filename").strip()
+
+ # 다음 줄에 코드 블록 "```\n(대체 텍스트 by Upstage)"가 있으면 이미 처리된 것으로 간주
+ match_end = match.end()
+ remaining_content = md_content[match_end:]
+
+ if remaining_content.startswith("\n```\n(대체 텍스트 by Upstage)"):
+ print(f"[INFO] '{filename}'은 이미 대체 텍스트가 있습니다. 건너뜁니다.")
+ continue
+
+ # Vault 전체에서 이미지 파일 찾기
+ image_full_path = self._find_image_in_vault(filename, vault_root)
+
+ if image_full_path:
+ images_to_process.append(
+ {
+ "src": filename,
+ "path": image_full_path,
+ "match_object": match,
+ }
+ )
+ else:
+ print(
+ f"[WARNING] Vault 경로에서 이미지 파일 '{filename}'을 찾을 수 없습니다."
+ )
+
+ return images_to_process
+
+
+if __name__ == "__main__":
+ # 테스트 코드
+ from dotenv import load_dotenv
+
+ load_dotenv()
+
+ processor = MarkdownImageProcessor()
+
+ # 테스트용 마크다운 내용 (Obsidian 위키링크 형식)
+ test_md_content = """
+# 테스트 노트
+
+이것은 테스트 이미지입니다.
+
+![[test.png]]
+사용자가 작성한 노트 내용...
+
+다른 이미지:
+
+![[diagram.jpg]]
+이미 작성된 내용이 있어도 대체 텍스트는 코드 블록으로 구분됩니다.
+"""
+
+ test_vault_path = Path("/path/to/vault")
+
+ def test_progress_callback(current: int, total: int, img_src: str):
+ print(f"[PROGRESS] {current}/{total} - {img_src}")
+
+ # 처리 실행
+ if test_vault_path.exists():
+ updated_content, processed = processor.process_images(
+ test_md_content, test_vault_path, test_progress_callback
+ )
+ print(f"\n[INFO] 업데이트된 내용:\n{updated_content}")
+ print(f"\n[INFO] 처리된 이미지: {len(processed)}개")
+ else:
+ print(f"[WARNING] 테스트 Vault 경로를 찾을 수 없습니다: {test_vault_path}")
diff --git a/usecase/solar-knowledge-management/backend/image_ocr/ocr_processor.py b/usecase/solar-knowledge-management/backend/image_ocr/ocr_processor.py
new file mode 100644
index 0000000..fdcb09d
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/image_ocr/ocr_processor.py
@@ -0,0 +1,128 @@
+"""
+OCR 및 문서 구조 추출 API 호출 모듈
+"""
+
+import os
+import requests
+from pathlib import Path
+from typing import Dict, Optional
+from dotenv import load_dotenv
+
+load_dotenv()
+
+
+class OCRProcessor:
+ """Upstage Document Parse API를 사용하여 이미지에서 OCR 텍스트 및 구조를 추출"""
+
+ def __init__(self):
+ """
+ OCRProcessor 초기화
+
+ Raises:
+ ValueError: UPSTAGE_API_KEY 환경 변수가 설정되지 않은 경우
+ """
+ self.api_key: Optional[str] = os.getenv("UPSTAGE_API_KEY")
+ if not self.api_key:
+ raise ValueError("UPSTAGE_API_KEY 환경 변수가 설정되지 않았습니다")
+
+ self.ocr_endpoint: str = "https://api.upstage.ai/v1/document-digitization"
+
+ def extract_text(self, image_path: Path) -> str:
+ """
+ Upstage Document Parse API를 사용하여 이미지에서 OCR 텍스트 및 구조를 추출합니다.
+
+ Args:
+ image_path: 추출할 이미지 파일 경로
+
+ Returns:
+ 추출된 텍스트 (오류 시 ERROR 메시지)
+
+ Raises:
+ requests.exceptions.HTTPError: HTTP 요청 실패 시
+ Exception: 기타 처리 오류 시
+ """
+ try:
+ # 요청 헤더 구성
+ headers: Dict[str, str] = {"Authorization": f"Bearer {self.api_key}"}
+ mime_type = "image/" + image_path.suffix.lstrip(".")
+
+ # 파일을 with 문으로 안전하게 열기
+ with open(image_path, "rb") as image_file:
+ files = {
+ "document": (
+ image_path.name,
+ image_file,
+ mime_type,
+ )
+ }
+
+ # 공식 데이터 파라미터
+ data_payload: Dict = {
+ "model": "document-parse",
+ "ocr": "force", # OCR 강제 실행
+ "output_formats": "['markdown', 'text']", # 출력 형식 명시
+ "base64_encoding": "['table', 'figure']", # 테이블과 이미지 포함
+ }
+
+ response = requests.post(
+ self.ocr_endpoint,
+ headers=headers,
+ files=files,
+ data=data_payload,
+ timeout=120,
+ )
+
+ response.raise_for_status()
+ response_json: Dict = response.json()
+
+ # Document Parse 결과에서 추출된 텍스트를 반환
+ # 1. 최상위 content 객체에서 text 추출 (우선순위: text > markdown)
+ content = response_json.get("content", {})
+ extracted_text = content.get("text", "").strip() or content.get("markdown", "").strip()
+
+ # 2. content가 없으면 elements 배열에서 추출
+ if not extracted_text and "elements" in response_json:
+ elements = response_json.get("elements", [])
+ text_parts = []
+
+ for element in elements:
+ element_content = element.get("content", {})
+ # 각 element의 텍스트 추출 (우선순위: text > markdown)
+ element_text = element_content.get("text", "").strip() or element_content.get("markdown", "").strip()
+
+ if element_text:
+ text_parts.append(element_text)
+
+ extracted_text = "\n\n".join(text_parts)
+
+ # 디버깅: 추출된 텍스트 길이 출력
+ print(f"[INFO] OCR 결과: {len(extracted_text)}자 추출 (파일: {image_path.name})")
+ if not extracted_text:
+ print(f"[WARNING] OCR이 빈 텍스트를 반환했습니다.")
+ print(f"[DEBUG] API 응답 구조: content={bool(content)}, elements={len(response_json.get('elements', []))}개")
+
+ return extracted_text
+
+ except requests.exceptions.HTTPError as e:
+ error_msg = (
+ f"ERROR: OCR/Parse API 요청 실패 (HTTP {e.response.status_code})"
+ )
+ print(f"[ERROR] {error_msg}")
+ print(f"[ERROR] 응답 상세: {e.response.text}")
+ return f"{error_msg}: {e.response.text[:100]}"
+ except Exception as e:
+ error_msg = f"ERROR: OCR Processing Failed - {str(e)}"
+ print(f"[ERROR] {error_msg}")
+ return error_msg
+
+
+if __name__ == "__main__":
+ # 테스트 코드
+ processor = OCRProcessor()
+ test_image_path = Path("test_image.png")
+
+ if test_image_path.exists():
+ result = processor.extract_text(test_image_path)
+ print(f"[INFO] OCR 결과: {result[:200]}...")
+ else:
+ print(f"[WARNING] 테스트 이미지 파일을 찾을 수 없습니다: {test_image_path}")
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/__init__.py b/usecase/solar-knowledge-management/backend/note_freshness/__init__.py
new file mode 100644
index 0000000..fb0f408
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/__init__.py
@@ -0,0 +1 @@
+"""Note freshness check module for checking and updating note recency."""
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/api/__init__.py b/usecase/solar-knowledge-management/backend/note_freshness/api/__init__.py
new file mode 100644
index 0000000..17de443
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/api/__init__.py
@@ -0,0 +1,6 @@
+"""External API clients for note freshness module."""
+
+from .wikipedia import WikipediaClient
+from .tavily import TavilyClient
+
+__all__ = ["WikipediaClient", "TavilyClient"]
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/api/tavily.py b/usecase/solar-knowledge-management/backend/note_freshness/api/tavily.py
new file mode 100644
index 0000000..7398ef0
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/api/tavily.py
@@ -0,0 +1,105 @@
+"""Tavily Search API client."""
+
+import httpx
+from typing import Optional, Dict, Any, List
+from datetime import datetime
+from ..config import Config
+
+
+class TavilyClient:
+ """Client for Tavily Search API."""
+
+ BASE_URL = "https://api.tavily.com/search"
+
+ def __init__(self, api_key: Optional[str] = None):
+ """Initialize Tavily client.
+
+ Args:
+ api_key: Tavily API key (defaults to Config.TAVILY_API_KEY)
+ """
+ self.api_key = api_key or Config.TAVILY_API_KEY
+ if not self.api_key:
+ raise ValueError("TAVILY_API_KEY is required")
+
+ def search(
+ self,
+ query: str,
+ search_depth: str = "basic",
+ max_results: int = 5,
+ include_answer: bool = False,
+ include_raw_content: bool = False,
+ ) -> Optional[Dict[str, Any]]:
+ """Search using Tavily API.
+
+ Args:
+ query: Search query
+ search_depth: "basic" or "advanced"
+ max_results: Maximum number of results
+ include_answer: Include AI-generated answer
+ include_raw_content: Include raw page content
+
+ Returns:
+ Search results dictionary or None on error
+ """
+ payload = {
+ "api_key": self.api_key,
+ "query": query,
+ "search_depth": search_depth,
+ "max_results": max_results,
+ "include_answer": include_answer,
+ "include_raw_content": include_raw_content,
+ }
+
+ try:
+ with httpx.Client(timeout=60.0) as client:
+ response = client.post(self.BASE_URL, json=payload)
+ response.raise_for_status()
+ data = response.json()
+
+ return {
+ "query": query,
+ "results": data.get("results", []),
+ "answer": data.get("answer", ""),
+ "searched_at": datetime.now().isoformat(),
+ }
+ except httpx.HTTPStatusError as e:
+ print(f"HTTP error during Tavily search: {e}")
+ return None
+ except Exception as e:
+ print(f"Error during Tavily search: {e}")
+ return None
+
+ def search_and_parse(
+ self, query: str, max_results: int = 3
+ ) -> Optional[Dict[str, Any]]:
+ """Search and parse results for freshness check.
+
+ Args:
+ query: Search query
+ max_results: Maximum results to return
+
+ Returns:
+ Parsed search results with query, results, and searched_at
+ """
+ result = self.search(query, max_results=max_results)
+ if not result:
+ return None
+
+ # Parse and limit results
+ parsed_results = []
+ for item in result.get("results", [])[:max_results]:
+ parsed_results.append(
+ {
+ "title": item.get("title", ""),
+ "url": item.get("url", ""),
+ "content": item.get("content", ""),
+ "score": item.get("score", 0),
+ "published_date": item.get("published_date", ""),
+ }
+ )
+
+ return {
+ "query": query,
+ "results": parsed_results,
+ "searched_at": result["searched_at"],
+ }
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/api/wikipedia.py b/usecase/solar-knowledge-management/backend/note_freshness/api/wikipedia.py
new file mode 100644
index 0000000..726a849
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/api/wikipedia.py
@@ -0,0 +1,86 @@
+"""Wikipedia API client for searching and retrieving articles."""
+
+import wikipedia
+from typing import Optional, Dict, Any
+from datetime import datetime
+
+
+class WikipediaClient:
+ """Client for Wikipedia API using wikipedia package."""
+
+ def __init__(self, language: str = "ko"):
+ """Initialize Wikipedia client.
+
+ Args:
+ language: Wikipedia language code (default: ko for Korean)
+ """
+ self.language = language
+ wikipedia.set_lang(language)
+
+ def search_and_get_summary(self, keyword: str) -> Optional[Dict[str, Any]]:
+ """Search for a keyword and get the summary of the top result.
+
+ Args:
+ keyword: Search keyword
+
+ Returns:
+ Dictionary with keyword, title, summary, url, and searched_at
+ """
+ result = {
+ "keyword": keyword,
+ "wiki_exists": False,
+ "title": "",
+ "summary": "Wikipedia에서 해당 키워드에 대한 문서 요약 정보를 찾지 못함",
+ "url": "",
+ "searched_at": datetime.now().isoformat(),
+ }
+
+ try:
+ # Search for keyword (get top 1 result)
+ page_titles = wikipedia.search(keyword, results=1)
+
+ if not page_titles:
+ print(f"문서 미존재: '{keyword}' 검색 결과가 없습니다.")
+ return result
+
+ page_title = page_titles[0]
+
+ # Get page summary
+ page_summary = wikipedia.summary(
+ page_title, sentences=3, auto_suggest=False
+ )
+
+ # Get page for URL
+ try:
+ page = wikipedia.page(page_title, auto_suggest=False)
+ url = page.url
+ except Exception:
+ url = f"https://{self.language}.wikipedia.org/wiki/{page_title.replace(' ', '_')}"
+
+ # Update result
+ result["wiki_exists"] = True
+ result["title"] = page_title
+ result["summary"] = page_summary
+ result["url"] = url
+
+ print(f"문서 존재: '{page_title}'")
+ return result
+
+ except wikipedia.exceptions.PageError:
+ print(f"문서 미존재: '{keyword}' - 제목이 정확히 일치하는 문서가 없습니다.")
+ return result
+ except wikipedia.exceptions.DisambiguationError as e:
+ print(f"모호성: '{keyword}' - 여러 문서가 검색됨. 옵션: {e.options[:3]}")
+ result["wiki_exists"] = True
+ result["title"] = keyword
+ result["summary"] = (
+ f"모호성 해소 필요: 다음 중 하나를 선택해야 합니다: {e.options[:3]}"
+ )
+ result["url"] = (
+ f"https://{self.language}.wikipedia.org/wiki/{keyword.replace(' ', '_')}"
+ )
+ return result
+ except Exception as e:
+ print(f"Wikipedia API 오류 발생: {e}")
+ result["summary"] = f"Wikipedia API 처리 중 오류 발생: {e}"
+ return result
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/config.py b/usecase/solar-knowledge-management/backend/note_freshness/config.py
new file mode 100644
index 0000000..20806cb
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/config.py
@@ -0,0 +1,81 @@
+"""Configuration management for note freshness module."""
+
+import os
+from pathlib import Path
+from dotenv import load_dotenv
+
+load_dotenv()
+
+
+def _get_project_root() -> Path:
+ """Get the project root directory."""
+ current = Path(__file__).resolve()
+ for level in range(4):
+ candidate = current.parents[level]
+ if (candidate / "pyproject.toml").exists():
+ return candidate
+ return current.parent.parent.parent.parent
+
+
+PROJECT_ROOT = _get_project_root()
+
+
+class Config:
+ """Application configuration manager for note freshness."""
+
+ # Upstage API settings
+ UPSTAGE_API_KEY: str = os.getenv("UPSTAGE_API_KEY", "")
+ UPSTAGE_API_BASE: str = os.getenv(
+ "UPSTAGE_API_BASE", "https://api.upstage.ai/v1/solar"
+ )
+ MODEL_NAME: str = os.getenv("MODEL_NAME", "solar-pro2")
+
+ # Information Extraction API
+ UPSTAGE_IE_API_BASE: str = "https://api.upstage.ai/v1/information-extraction"
+
+ # Tavily API
+ TAVILY_API_KEY: str = os.getenv("TAVILY_API_KEY", "")
+
+ # Application settings
+ PROMPTS_DIR: Path = Path(os.getenv("PROMPTS_DIR", str(PROJECT_ROOT / "prompts")))
+ DATA_DIR: Path = PROJECT_ROOT / "data"
+
+ # LLM settings
+ DEFAULT_TEMPERATURE: float = float(os.getenv("DEFAULT_TEMPERATURE", "0.7"))
+ DEFAULT_MAX_TOKENS: int = int(os.getenv("DEFAULT_MAX_TOKENS", "8192"))
+
+ # HTTP client timeout settings
+ HTTP_TIMEOUT_ASYNC: float = float(os.getenv("HTTP_TIMEOUT_ASYNC", "120.0"))
+ HTTP_TIMEOUT_SYNC: float = float(os.getenv("HTTP_TIMEOUT_SYNC", "120.0"))
+
+ @classmethod
+ def validate(cls) -> bool:
+ """Validate that required configuration is present."""
+ if not cls.UPSTAGE_API_KEY:
+ return False
+ return True
+
+ @classmethod
+ def validate_tavily(cls) -> bool:
+ """Validate Tavily API key."""
+ return bool(cls.TAVILY_API_KEY)
+
+ @classmethod
+ def get_freshness_folder(cls, raw_note_path: Path) -> Path:
+ """Generate the folder path for freshness data.
+
+ Args:
+ raw_note_path: Path to the raw note file
+
+ Returns:
+ Path to the freshness data folder
+ """
+ parent = raw_note_path.parent
+ stem = raw_note_path.stem
+ return parent / stem
+
+ @classmethod
+ def ensure_directories(cls):
+ """Ensure required directories exist."""
+ cls.PROMPTS_DIR.mkdir(parents=True, exist_ok=True)
+ cls.DATA_DIR.mkdir(parents=True, exist_ok=True)
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/core/__init__.py b/usecase/solar-knowledge-management/backend/note_freshness/core/__init__.py
new file mode 100644
index 0000000..59c506c
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/core/__init__.py
@@ -0,0 +1,7 @@
+"""Core utilities for note freshness module."""
+
+from .path_utils import resolve_path, format_path_for_display
+from .state_manager import StateManager
+from .file_handler import FileHandler
+
+__all__ = ["resolve_path", "format_path_for_display", "StateManager", "FileHandler"]
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/core/file_handler.py b/usecase/solar-knowledge-management/backend/note_freshness/core/file_handler.py
new file mode 100644
index 0000000..87e0d8a
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/core/file_handler.py
@@ -0,0 +1,233 @@
+"""File handling operations for note freshness."""
+
+import re
+import yaml
+from pathlib import Path
+from typing import Optional, Tuple, Union
+from datetime import datetime
+from ..models import FreshnessMetadata
+from .path_utils import resolve_path
+
+
+class FileHandler:
+ """Handler for file operations related to note freshness."""
+
+ @staticmethod
+ def _resolve_path(path: Union[str, Path]) -> Path:
+ return resolve_path(path)
+
+ @staticmethod
+ def read_note(
+ note_path: Union[str, Path],
+ ) -> Tuple[Optional[str], Optional[FreshnessMetadata]]:
+ """Read a markdown note file and extract metadata.
+
+ Args:
+ note_path: Path to the note file
+
+ Returns:
+ Tuple of (full content as string, FreshnessMetadata) or (None, None) on error
+ """
+ try:
+ resolved_path = FileHandler._resolve_path(note_path)
+ with open(resolved_path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ # Extract YAML front matter
+ metadata = FileHandler._extract_yaml_metadata(content)
+ return content, metadata
+ except FileNotFoundError:
+ print(f"File not found: {note_path}")
+ return None, None
+ except Exception as e:
+ print(f"Error reading file {note_path}: {e}")
+ return None, None
+
+ @staticmethod
+ def _extract_yaml_metadata(content: str) -> FreshnessMetadata:
+ """Extract freshness-related metadata from YAML front matter."""
+ metadata = FreshnessMetadata()
+
+ # Match YAML front matter
+ yaml_match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL)
+ if yaml_match:
+ try:
+ yaml_content = yaml.safe_load(yaml_match.group(1))
+ if yaml_content:
+ # Extract info_keyword
+ info_keyword = yaml_content.get("info_keyword", [])
+ if isinstance(info_keyword, str):
+ info_keyword = [info_keyword]
+ metadata.info_keyword = info_keyword
+
+ # Extract info_query
+ info_query = yaml_content.get("info_query", [])
+ if isinstance(info_query, str):
+ info_query = [info_query]
+ metadata.info_query = info_query
+
+ # Extract search timestamps
+ metadata.wiki_searched_at = yaml_content.get("wiki_searched_at")
+ metadata.tavily_searched_at = yaml_content.get("tavily_searched_at")
+ except yaml.YAMLError as e:
+ print(f"Error parsing YAML metadata: {e}")
+
+ return metadata
+
+ @staticmethod
+ def update_note_metadata(
+ note_path: Union[str, Path],
+ info_keyword: list = None,
+ info_query: list = None,
+ wiki_searched_at: str = None,
+ tavily_searched_at: str = None,
+ ) -> bool:
+ """Update the YAML front matter of a note with freshness metadata.
+
+ Args:
+ note_path: Path to the note file
+ info_keyword: List of keywords for Wikipedia search
+ info_query: List of queries for Tavily search
+ wiki_searched_at: Timestamp of Wikipedia search
+ tavily_searched_at: Timestamp of Tavily search
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ resolved_path = FileHandler._resolve_path(note_path)
+ with open(resolved_path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ # Check if YAML front matter exists
+ yaml_match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL)
+
+ if yaml_match:
+ # Parse existing YAML
+ try:
+ yaml_content = yaml.safe_load(yaml_match.group(1)) or {}
+ except yaml.YAMLError:
+ yaml_content = {}
+
+ # Update metadata
+ if info_keyword is not None:
+ yaml_content["info_keyword"] = info_keyword
+ if info_query is not None:
+ yaml_content["info_query"] = info_query
+ if wiki_searched_at is not None:
+ yaml_content["wiki_searched_at"] = wiki_searched_at
+ if tavily_searched_at is not None:
+ yaml_content["tavily_searched_at"] = tavily_searched_at
+
+ # Reconstruct content with updated YAML
+ new_yaml = yaml.dump(
+ yaml_content,
+ allow_unicode=True,
+ default_flow_style=False,
+ sort_keys=False,
+ )
+ new_content = f"---\n{new_yaml}---{content[yaml_match.end():]}"
+ else:
+ # Create new YAML front matter
+ yaml_content = {}
+ if info_keyword is not None:
+ yaml_content["info_keyword"] = info_keyword
+ if info_query is not None:
+ yaml_content["info_query"] = info_query
+ if wiki_searched_at is not None:
+ yaml_content["wiki_searched_at"] = wiki_searched_at
+ if tavily_searched_at is not None:
+ yaml_content["tavily_searched_at"] = tavily_searched_at
+
+ new_yaml = yaml.dump(
+ yaml_content,
+ allow_unicode=True,
+ default_flow_style=False,
+ sort_keys=False,
+ )
+ new_content = f"---\n{new_yaml}---\n\n{content}"
+
+ # Write back to file
+ with open(resolved_path, "w", encoding="utf-8") as f:
+ f.write(new_content)
+ return True
+ except Exception as e:
+ print(f"Error updating note metadata: {e}")
+ return False
+
+ @staticmethod
+ def save_search_result(
+ save_folder: Union[str, Path], filename: str, content: str
+ ) -> bool:
+ """Save search result to a markdown file.
+
+ Args:
+ save_folder: Folder to save the result
+ filename: Name of the file (without extension)
+ content: Markdown content to save
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ resolved_folder = FileHandler._resolve_path(save_folder)
+ resolved_folder.mkdir(parents=True, exist_ok=True)
+
+ file_path = resolved_folder / f"{filename}.md"
+ with open(file_path, "w", encoding="utf-8") as f:
+ f.write(content)
+ return True
+ except Exception as e:
+ print(f"Error saving search result: {e}")
+ return False
+
+ @staticmethod
+ def insert_freshness_guide(
+ note_path: Union[str, Path], guide_summary: str, full_guide_path: str
+ ) -> bool:
+ """Insert freshness guide summary at the top of the note (after YAML).
+
+ Args:
+ note_path: Path to the note file
+ guide_summary: Summary of the freshness guide
+ full_guide_path: Relative path to the full guide
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ resolved_path = FileHandler._resolve_path(note_path)
+ with open(resolved_path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ # Find end of YAML front matter
+ yaml_match = re.match(r"^---\s*\n.*?\n---\s*\n?", content, re.DOTALL)
+
+ # Create the guide block
+ guide_block = f"""
+> [!info] 최신성 검토 가이드
+> {guide_summary}
+>
+> [[{full_guide_path}|전체 가이드 보기]]
+
+"""
+
+ if yaml_match:
+ # Insert after YAML
+ insert_pos = yaml_match.end()
+ new_content = content[:insert_pos] + guide_block + content[insert_pos:]
+ else:
+ # Insert at the beginning
+ new_content = guide_block + content
+
+ with open(resolved_path, "w", encoding="utf-8") as f:
+ f.write(new_content)
+ return True
+ except Exception as e:
+ print(f"Error inserting freshness guide: {e}")
+ return False
+
+ @staticmethod
+ def get_current_timestamp() -> str:
+ """Get current timestamp in ISO format."""
+ return datetime.now().isoformat()
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/core/path_utils.py b/usecase/solar-knowledge-management/backend/note_freshness/core/path_utils.py
new file mode 100644
index 0000000..1ae68fb
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/core/path_utils.py
@@ -0,0 +1,9 @@
+"""Path utilities - re-export from note_split."""
+
+from backend.note_split.core.path_utils import (
+ resolve_path,
+ format_path_for_display,
+ normalize_path_for_wsl,
+)
+
+__all__ = ["resolve_path", "format_path_for_display", "normalize_path_for_wsl"]
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/core/state_manager.py b/usecase/solar-knowledge-management/backend/note_freshness/core/state_manager.py
new file mode 100644
index 0000000..e1de3a9
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/core/state_manager.py
@@ -0,0 +1,182 @@
+"""State management for Streamlit session state."""
+
+from typing import List, Optional
+import streamlit as st
+from pathlib import Path
+from ..models import FreshnessMetadata, DescriptionTemplate
+from .path_utils import resolve_path
+
+
+class StateManager:
+ """Manager for Streamlit session state for note freshness."""
+
+ # State keys
+ KEY_RAW_NOTE_PATH = "freshness_raw_note_path"
+ KEY_SAVE_FOLDER_PATH = "freshness_save_folder_path"
+ KEY_RAW_NOTE_CONTENT = "freshness_raw_note_content"
+ KEY_METADATA = "freshness_metadata"
+ KEY_INFO_KEYWORD = "freshness_info_keyword"
+ KEY_INFO_QUERY = "freshness_info_query"
+ KEY_DESCRIPTION_TEMPLATE = "freshness_desc_template"
+ KEY_WIKI_RESULTS = "freshness_wiki_results"
+ KEY_TAVILY_RESULTS = "freshness_tavily_results"
+ KEY_STEP = "freshness_current_step"
+
+ # Step identifiers
+ STEP_INIT = "init"
+ STEP_NOTE_VALIDATED = "note_validated"
+ STEP_TEMPLATE_SELECT = "template_select"
+ STEP_EXTRACTION_DONE = "extraction_done"
+ STEP_METADATA_CONFIRMED = "metadata_confirmed"
+ STEP_SEARCH_DONE = "search_done"
+ STEP_GUIDE_GENERATED = "guide_generated"
+
+ @staticmethod
+ def initialize():
+ """Initialize session state with default values."""
+ if StateManager.KEY_STEP not in st.session_state:
+ st.session_state[StateManager.KEY_STEP] = StateManager.STEP_INIT
+
+ if StateManager.KEY_RAW_NOTE_PATH not in st.session_state:
+ st.session_state[StateManager.KEY_RAW_NOTE_PATH] = None
+
+ if StateManager.KEY_SAVE_FOLDER_PATH not in st.session_state:
+ st.session_state[StateManager.KEY_SAVE_FOLDER_PATH] = None
+
+ if StateManager.KEY_RAW_NOTE_CONTENT not in st.session_state:
+ st.session_state[StateManager.KEY_RAW_NOTE_CONTENT] = None
+
+ if StateManager.KEY_METADATA not in st.session_state:
+ st.session_state[StateManager.KEY_METADATA] = None
+
+ if StateManager.KEY_INFO_KEYWORD not in st.session_state:
+ st.session_state[StateManager.KEY_INFO_KEYWORD] = []
+
+ if StateManager.KEY_INFO_QUERY not in st.session_state:
+ st.session_state[StateManager.KEY_INFO_QUERY] = []
+
+ if StateManager.KEY_DESCRIPTION_TEMPLATE not in st.session_state:
+ st.session_state[StateManager.KEY_DESCRIPTION_TEMPLATE] = None
+
+ if StateManager.KEY_WIKI_RESULTS not in st.session_state:
+ st.session_state[StateManager.KEY_WIKI_RESULTS] = []
+
+ if StateManager.KEY_TAVILY_RESULTS not in st.session_state:
+ st.session_state[StateManager.KEY_TAVILY_RESULTS] = []
+
+ @staticmethod
+ def get_current_step() -> str:
+ return st.session_state.get(StateManager.KEY_STEP, StateManager.STEP_INIT)
+
+ @staticmethod
+ def set_step(step: str):
+ st.session_state[StateManager.KEY_STEP] = step
+
+ @staticmethod
+ def get_raw_note_path() -> Optional[Path]:
+ path = st.session_state.get(StateManager.KEY_RAW_NOTE_PATH)
+ if path:
+ return resolve_path(path)
+ return None
+
+ @staticmethod
+ def set_raw_note_path(path: Path):
+ st.session_state[StateManager.KEY_RAW_NOTE_PATH] = str(path)
+
+ @staticmethod
+ def get_save_folder_path() -> Optional[Path]:
+ path = st.session_state.get(StateManager.KEY_SAVE_FOLDER_PATH)
+ if path:
+ return resolve_path(path)
+ return None
+
+ @staticmethod
+ def set_save_folder_path(path: Path):
+ st.session_state[StateManager.KEY_SAVE_FOLDER_PATH] = str(path)
+
+ @staticmethod
+ def get_raw_note_content() -> Optional[str]:
+ return st.session_state.get(StateManager.KEY_RAW_NOTE_CONTENT)
+
+ @staticmethod
+ def set_raw_note_content(content: str):
+ st.session_state[StateManager.KEY_RAW_NOTE_CONTENT] = content
+
+ @staticmethod
+ def get_metadata() -> Optional[FreshnessMetadata]:
+ data = st.session_state.get(StateManager.KEY_METADATA)
+ if data:
+ return FreshnessMetadata(
+ info_keyword=data.get("info_keyword", []),
+ info_query=data.get("info_query", []),
+ wiki_searched_at=data.get("wiki_searched_at"),
+ tavily_searched_at=data.get("tavily_searched_at"),
+ )
+ return None
+
+ @staticmethod
+ def set_metadata(metadata: FreshnessMetadata):
+ st.session_state[StateManager.KEY_METADATA] = metadata.to_yaml_dict()
+
+ @staticmethod
+ def get_info_keyword() -> List[str]:
+ return st.session_state.get(StateManager.KEY_INFO_KEYWORD, [])
+
+ @staticmethod
+ def set_info_keyword(keywords: List[str]):
+ st.session_state[StateManager.KEY_INFO_KEYWORD] = keywords
+
+ @staticmethod
+ def get_info_query() -> List[str]:
+ return st.session_state.get(StateManager.KEY_INFO_QUERY, [])
+
+ @staticmethod
+ def set_info_query(queries: List[str]):
+ st.session_state[StateManager.KEY_INFO_QUERY] = queries
+
+ @staticmethod
+ def get_description_template() -> Optional[DescriptionTemplate]:
+ data = st.session_state.get(StateManager.KEY_DESCRIPTION_TEMPLATE)
+ if data:
+ return DescriptionTemplate.from_dict(data)
+ return None
+
+ @staticmethod
+ def set_description_template(template: DescriptionTemplate):
+ st.session_state[StateManager.KEY_DESCRIPTION_TEMPLATE] = template.to_dict()
+
+ @staticmethod
+ def get_wiki_results() -> List[dict]:
+ return st.session_state.get(StateManager.KEY_WIKI_RESULTS, [])
+
+ @staticmethod
+ def set_wiki_results(results: List[dict]):
+ st.session_state[StateManager.KEY_WIKI_RESULTS] = results
+
+ @staticmethod
+ def get_tavily_results() -> List[dict]:
+ return st.session_state.get(StateManager.KEY_TAVILY_RESULTS, [])
+
+ @staticmethod
+ def set_tavily_results(results: List[dict]):
+ st.session_state[StateManager.KEY_TAVILY_RESULTS] = results
+
+ @staticmethod
+ def reset():
+ """Reset all state to initial values."""
+ keys_to_reset = [
+ StateManager.KEY_STEP,
+ StateManager.KEY_RAW_NOTE_PATH,
+ StateManager.KEY_SAVE_FOLDER_PATH,
+ StateManager.KEY_RAW_NOTE_CONTENT,
+ StateManager.KEY_METADATA,
+ StateManager.KEY_INFO_KEYWORD,
+ StateManager.KEY_INFO_QUERY,
+ StateManager.KEY_DESCRIPTION_TEMPLATE,
+ StateManager.KEY_WIKI_RESULTS,
+ StateManager.KEY_TAVILY_RESULTS,
+ ]
+ for key in keys_to_reset:
+ if key in st.session_state:
+ del st.session_state[key]
+ StateManager.initialize()
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/llm/__init__.py b/usecase/solar-knowledge-management/backend/note_freshness/llm/__init__.py
new file mode 100644
index 0000000..a4bc794
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/llm/__init__.py
@@ -0,0 +1,7 @@
+"""LLM utilities for note freshness module."""
+
+from .client import UpstageClient
+from .parsers import ResponseParser
+from .prompt_loader import PromptLoader
+
+__all__ = ["UpstageClient", "ResponseParser", "PromptLoader"]
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/llm/client.py b/usecase/solar-knowledge-management/backend/note_freshness/llm/client.py
new file mode 100644
index 0000000..53e4971
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/llm/client.py
@@ -0,0 +1,147 @@
+"""LLM client for Upstage API interaction including Information Extraction."""
+
+import base64
+import json
+import httpx
+from openai import OpenAI
+from typing import List, Optional, Dict, Any
+from pathlib import Path
+from ..config import Config
+
+
+class UpstageClient:
+ """Client for interacting with Upstage Solar API."""
+
+ def __init__(
+ self,
+ api_key: Optional[str] = None,
+ api_base: Optional[str] = None,
+ model: Optional[str] = None,
+ ):
+ self.api_key = api_key or Config.UPSTAGE_API_KEY
+ self.api_base = api_base or Config.UPSTAGE_API_BASE
+ self.model = model or Config.MODEL_NAME
+
+ if not self.api_key:
+ raise ValueError("UPSTAGE_API_KEY is required")
+
+ def _get_headers(self) -> Dict[str, str]:
+ return {
+ "Authorization": f"Bearer {self.api_key}",
+ "Content-Type": "application/json",
+ }
+
+ def _encode_file_to_base64(self, file_path: Path) -> str:
+ """Encode file to base64 string."""
+ with open(file_path, "rb") as f:
+ return base64.b64encode(f.read()).decode("utf-8")
+
+ def make_request_sync(
+ self,
+ messages: List[Dict[str, str]],
+ temperature: float = None,
+ max_tokens: int = None,
+ ) -> Optional[str]:
+ """Make a synchronous request to the Upstage API."""
+ temperature = temperature or Config.DEFAULT_TEMPERATURE
+ max_tokens = max_tokens or Config.DEFAULT_MAX_TOKENS
+
+ url = f"{self.api_base}/chat/completions"
+ payload = {
+ "model": self.model,
+ "messages": messages,
+ "temperature": temperature,
+ "max_tokens": max_tokens,
+ }
+
+ try:
+ with httpx.Client(timeout=Config.HTTP_TIMEOUT_SYNC) as client:
+ response = client.post(url, headers=self._get_headers(), json=payload)
+ response.raise_for_status()
+ data = response.json()
+ return data["choices"][0]["message"]["content"]
+ except httpx.HTTPStatusError as e:
+ print(f"HTTP error occurred: {e}")
+ return None
+ except Exception as e:
+ print(f"Error making request: {e}")
+ return None
+
+ def extract_information(
+ self, document_path: Path, schema: str
+ ) -> Optional[Dict[str, Any]]:
+ """Extract information from a document using Upstage Information Extraction API.
+
+ Args:
+ document_path: Path to the document file (docx)
+ schema: JSON schema string defining what to extract
+
+ Returns:
+ Extracted information as dictionary or None on error
+ """
+ try:
+ # Encode document to base64
+ base64_data = self._encode_file_to_base64(document_path)
+
+ # Create OpenAI client for Information Extraction
+ client = OpenAI(
+ api_key=self.api_key,
+ base_url="https://api.upstage.ai/v1/information-extraction",
+ )
+
+ # Parse schema string to dict
+ try:
+ schema_dict = json.loads(schema)
+ except json.JSONDecodeError:
+ print("Error: Invalid JSON schema")
+ return None
+
+ # Make extraction request
+ extraction_response = client.chat.completions.create(
+ model="information-extract",
+ messages=[
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "image_url",
+ "image_url": {
+ "url": f"data:application/octet-stream;base64,{base64_data}"
+ },
+ }
+ ],
+ }
+ ],
+ response_format={
+ "type": "json_schema",
+ "json_schema": {"name": "document_schema", "schema": schema_dict},
+ },
+ )
+
+ # Parse result
+ json_str = extraction_response.choices[0].message.content
+ result = json.loads(json_str)
+ return result
+
+ except Exception as e:
+ print(f"Error during information extraction: {e}")
+ return None
+
+ def generate_freshness_guide(
+ self, system_prompt: str, user_prompt: str, temperature: float = 0.3
+ ) -> Optional[str]:
+ """Generate freshness guide using Solar model.
+
+ Args:
+ system_prompt: System prompt
+ user_prompt: User prompt with context
+ temperature: Lower temperature for more focused output
+
+ Returns:
+ Generated guide content or None on error
+ """
+ messages = [
+ {"role": "system", "content": system_prompt},
+ {"role": "user", "content": user_prompt},
+ ]
+ return self.make_request_sync(messages, temperature=temperature)
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/llm/parsers.py b/usecase/solar-knowledge-management/backend/note_freshness/llm/parsers.py
new file mode 100644
index 0000000..ac66c96
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/llm/parsers.py
@@ -0,0 +1,96 @@
+"""Parsers for LLM responses."""
+
+import json
+import re
+from typing import List, Optional, Dict, Any
+
+
+class ResponseParser:
+ """Parser for LLM responses."""
+
+ @staticmethod
+ def parse_extraction_result(result: Dict[str, Any]) -> tuple[List[str], List[str]]:
+ """Parse information extraction result to get keywords and queries.
+
+ Args:
+ result: Raw extraction result from API
+
+ Returns:
+ Tuple of (info_keyword list, info_query list)
+ """
+ info_keyword = []
+ info_query = []
+
+ if not result:
+ return info_keyword, info_query
+
+ # Try to extract from various possible formats
+ if isinstance(result, dict):
+ # Direct extraction
+ if "info_keyword" in result:
+ kw = result["info_keyword"]
+ info_keyword = kw if isinstance(kw, list) else [kw]
+
+ if "info_query" in result:
+ q = result["info_query"]
+ info_query = q if isinstance(q, list) else [q]
+
+ # Try nested structure
+ if "extraction" in result:
+ return ResponseParser.parse_extraction_result(result["extraction"])
+
+ return info_keyword, info_query
+
+ @staticmethod
+ def parse_wiki_content(content: str) -> str:
+ """Parse Wikipedia content for markdown format."""
+ # Clean up content
+ content = re.sub(r"\[\[([^\]|]+)\|?[^\]]*\]\]", r"\1", content)
+ return content.strip()
+
+ @staticmethod
+ def parse_tavily_results(results: List[dict]) -> List[dict]:
+ """Parse Tavily search results.
+
+ Args:
+ results: Raw Tavily results
+
+ Returns:
+ List of parsed result dictionaries
+ """
+ parsed = []
+ for result in results[:3]: # Limit to top 3 results
+ parsed.append(
+ {
+ "title": result.get("title", ""),
+ "url": result.get("url", ""),
+ "content": result.get("content", ""),
+ "score": result.get("score", 0),
+ }
+ )
+ return parsed
+
+ @staticmethod
+ def extract_json_from_response(response: str) -> Optional[Dict[str, Any]]:
+ """Extract and parse JSON from a response."""
+ # Try to find JSON in code blocks first
+ json_match = re.search(r"```json\s*(.*?)\s*```", response, re.DOTALL)
+ if json_match:
+ try:
+ return json.loads(json_match.group(1))
+ except json.JSONDecodeError:
+ pass
+
+ # Try to find JSON object
+ json_match = re.search(r"\{.*\}", response, re.DOTALL)
+ if json_match:
+ try:
+ return json.loads(json_match.group(0))
+ except json.JSONDecodeError:
+ pass
+
+ # Try parsing entire response
+ try:
+ return json.loads(response)
+ except json.JSONDecodeError:
+ return None
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/llm/prompt_loader.py b/usecase/solar-knowledge-management/backend/note_freshness/llm/prompt_loader.py
new file mode 100644
index 0000000..7b2d82f
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/llm/prompt_loader.py
@@ -0,0 +1,91 @@
+"""Prompt template loading functionality for note freshness."""
+
+import yaml
+from pathlib import Path
+from typing import Dict, List, Optional
+from backend.note_split.models import PromptTemplate
+from ..config import Config
+
+
+class PromptLoader:
+ """Loader for prompt templates from YAML files."""
+
+ def __init__(self, prompts_dir: Optional[Path] = None):
+ """Initialize the prompt loader.
+
+ Args:
+ prompts_dir: Directory containing prompt template files
+ """
+ self.prompts_dir = prompts_dir or Config.PROMPTS_DIR
+
+ def load_template(self, template_name: str) -> Optional[PromptTemplate]:
+ """Load a specific prompt template by name.
+
+ Args:
+ template_name: Name of the template file (without .yml extension)
+
+ Returns:
+ PromptTemplate object or None if not found
+ """
+ template_path = self.prompts_dir / f"{template_name}.yml"
+
+ if not template_path.exists():
+ return None
+
+ try:
+ with open(template_path, "r", encoding="utf-8") as f:
+ data = yaml.safe_load(f)
+
+ if data is None:
+ print(f"Warning: Template file '{template_name}.yml' is empty.")
+ return None
+
+ if not isinstance(data, dict):
+ print(f"Error: Template {template_name} must be a dictionary.")
+ return None
+
+ return PromptTemplate(
+ name=data.get("name", template_name),
+ description=data.get("description", ""),
+ system_prompt=data.get("system_prompt", ""),
+ user_prompt_template=data.get("user_prompt_template", ""),
+ )
+ except yaml.YAMLError as e:
+ print(f"Error parsing YAML in template {template_name}: {e}")
+ return None
+ except Exception as e:
+ print(f"Error loading template {template_name}: {e}")
+ return None
+
+ def load_schema(self, schema_name: str) -> Optional[str]:
+ """Load a JSON schema from a YAML file.
+
+ Args:
+ schema_name: Name of the schema file (without .yml extension)
+
+ Returns:
+ Schema string or None if not found
+ """
+ schema_path = self.prompts_dir / f"{schema_name}.yml"
+
+ if not schema_path.exists():
+ return None
+
+ try:
+ with open(schema_path, "r", encoding="utf-8") as f:
+ data = yaml.safe_load(f)
+
+ if data is None or "schema" not in data:
+ return None
+
+ return data["schema"].strip()
+ except Exception as e:
+ print(f"Error loading schema {schema_name}: {e}")
+ return None
+
+ def list_templates(self) -> List[str]:
+ """List all available template names."""
+ if not self.prompts_dir.exists():
+ return []
+
+ return [p.stem for p in self.prompts_dir.glob("*.yml")]
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/models.py b/usecase/solar-knowledge-management/backend/note_freshness/models.py
new file mode 100644
index 0000000..0a36f8a
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/models.py
@@ -0,0 +1,99 @@
+"""Data models for the note freshness check module."""
+
+from dataclasses import dataclass, field
+from typing import List, Optional
+from datetime import datetime
+
+
+@dataclass
+class FreshnessMetadata:
+ """Represents metadata for freshness checking.
+
+ Attributes:
+ info_keyword: Keywords for Wikipedia search
+ info_query: Query strings for Tavily search
+ wiki_searched_at: Timestamp of last Wikipedia search
+ tavily_searched_at: Timestamp of last Tavily search
+ """
+
+ info_keyword: List[str] = field(default_factory=list)
+ info_query: List[str] = field(default_factory=list)
+ wiki_searched_at: Optional[str] = None
+ tavily_searched_at: Optional[str] = None
+
+ def to_yaml_dict(self) -> dict:
+ """Convert to dictionary for YAML serialization."""
+ result = {}
+ if self.info_keyword:
+ result["info_keyword"] = self.info_keyword
+ if self.info_query:
+ result["info_query"] = self.info_query
+ if self.wiki_searched_at:
+ result["wiki_searched_at"] = self.wiki_searched_at
+ if self.tavily_searched_at:
+ result["tavily_searched_at"] = self.tavily_searched_at
+ return result
+
+
+@dataclass
+class WikiSearchResult:
+ """Wikipedia search result.
+
+ Attributes:
+ keyword: Search keyword used
+ title: Article title
+ summary: Article summary
+ url: Article URL
+ searched_at: Search timestamp
+ """
+
+ keyword: str
+ title: str
+ summary: str
+ url: str
+ searched_at: str
+
+
+@dataclass
+class TavilySearchResult:
+ """Tavily search result.
+
+ Attributes:
+ query: Search query used
+ results: List of search result items
+ searched_at: Search timestamp
+ """
+
+ query: str
+ results: List[dict]
+ searched_at: str
+
+
+@dataclass
+class DescriptionTemplate:
+ """Template for information extraction description.
+
+ Attributes:
+ name: Template name
+ description: Template description for UI
+ content: The actual description content for API
+ """
+
+ name: str
+ description: str
+ content: str
+
+ def to_dict(self) -> dict:
+ return {
+ "name": self.name,
+ "description": self.description,
+ "content": self.content,
+ }
+
+ @classmethod
+ def from_dict(cls, data: dict) -> "DescriptionTemplate":
+ return cls(
+ name=data.get("name", ""),
+ description=data.get("description", ""),
+ content=data.get("content", ""),
+ )
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/ui/__init__.py b/usecase/solar-knowledge-management/backend/note_freshness/ui/__init__.py
new file mode 100644
index 0000000..a7d5a26
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/ui/__init__.py
@@ -0,0 +1,21 @@
+"""UI components for note freshness module."""
+
+from .components import (
+ render_file_input_section,
+ render_template_selection_section,
+ render_metadata_review_section,
+ render_search_results_section,
+ render_error,
+ render_success,
+ render_info,
+)
+
+__all__ = [
+ "render_file_input_section",
+ "render_template_selection_section",
+ "render_metadata_review_section",
+ "render_search_results_section",
+ "render_error",
+ "render_success",
+ "render_info",
+]
diff --git a/usecase/solar-knowledge-management/backend/note_freshness/ui/components.py b/usecase/solar-knowledge-management/backend/note_freshness/ui/components.py
new file mode 100644
index 0000000..5b128f0
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_freshness/ui/components.py
@@ -0,0 +1,124 @@
+"""Reusable UI components for Streamlit application."""
+
+import streamlit as st
+from typing import List, Optional, Dict
+
+
+def render_file_input_section():
+ """Render the file input section."""
+ st.markdown("## 1. 노트 검증")
+
+ note_path = st.text_input(
+ "노트 경로",
+ placeholder="/path/to/your/note.md",
+ help="최신성을 검토할 마크다운 노트 파일의 경로",
+ )
+
+ save_folder = st.text_input(
+ "저장 폴더 경로 (선택사항)",
+ placeholder="미입력시 위에서 입력한 노트가 있는 경로에 저장됩니다",
+ help="검색 결과와 가이드를 저장할 폴더",
+ )
+
+ return note_path, save_folder
+
+
+def render_template_selection_section(default_template: str = ""):
+ """Render the template selection and editing section."""
+ st.markdown("## 2. 추출 템플릿 설정")
+
+ st.markdown(
+ """
+ 아래 템플릿은 노트에서 최신성 검토를 위한 키워드와 쿼리를 추출하는 데 사용됩니다.
+ 필요에 따라 수정할 수 있습니다.
+ """
+ )
+
+ template_content = st.text_area(
+ "추출 설명 템플릿",
+ value=default_template,
+ height=300,
+ help="Upstage Information Extraction API에 전달할 설명",
+ )
+
+ return template_content
+
+
+def render_metadata_review_section(keywords: List[str], queries: List[str]):
+ """Render the metadata review and editing section."""
+ st.markdown("## 3. 추출 결과 검토")
+
+ st.markdown("추출된 키워드와 쿼리를 검토하고 필요시 수정하세요.")
+
+ # Keywords editing
+ st.markdown("### 검색 키워드 (Wikipedia)")
+ keywords_text = st.text_area(
+ "키워드 (한 줄에 하나씩)",
+ value="\n".join(keywords),
+ height=150,
+ help="Wikipedia 검색에 사용할 키워드",
+ )
+ edited_keywords = [kw.strip() for kw in keywords_text.split("\n") if kw.strip()]
+
+ # Queries editing
+ st.markdown("### 검색 쿼리 (Tavily)")
+ queries_text = st.text_area(
+ "쿼리 (한 줄에 하나씩)",
+ value="\n".join(queries),
+ height=150,
+ help="Tavily 검색에 사용할 쿼리",
+ )
+ edited_queries = [q.strip() for q in queries_text.split("\n") if q.strip()]
+
+ return edited_keywords, edited_queries
+
+
+def render_search_results_section(wiki_results: List[dict], tavily_results: List[dict]):
+ """Render the search results section."""
+ st.markdown("## 4. 검색 결과")
+
+ # Wikipedia results
+ if wiki_results:
+ st.markdown("### Wikipedia 검색 결과")
+ for result in wiki_results:
+ with st.expander(
+ f"📖 {result.get('title', 'Unknown')} ({result.get('keyword', '')})"
+ ):
+ st.markdown(f"**요약:** {result.get('summary', 'N/A')[:500]}...")
+ if result.get("url"):
+ st.markdown(f"[Wikipedia 링크]({result['url']})")
+
+ # Tavily results
+ if tavily_results:
+ st.markdown("### Tavily 검색 결과")
+ for result in tavily_results:
+ query = result.get("query", "")
+ st.markdown(f"#### 쿼리: {query}")
+ for item in result.get("results", []):
+ with st.expander(f"🔍 {item.get('title', 'Unknown')}"):
+ st.markdown(f"**내용:** {item.get('content', 'N/A')[:500]}...")
+ if item.get("url"):
+ st.markdown(f"[원본 링크]({item['url']})")
+
+
+def render_guide_preview(guide_content: str):
+ """Render the generated guide preview."""
+ st.markdown("## 5. 최신성 검토 가이드")
+
+ with st.expander("가이드 전체 보기", expanded=True):
+ st.markdown(guide_content)
+
+
+def render_error(message: str):
+ """Render an error message."""
+ st.error(f"❌ {message}")
+
+
+def render_success(message: str):
+ """Render a success message."""
+ st.success(f"✅ {message}")
+
+
+def render_info(message: str):
+ """Render an info message."""
+ st.info(f"ℹ️ {message}")
diff --git a/usecase/solar-knowledge-management/backend/note_split/__init__.py b/usecase/solar-knowledge-management/backend/note_split/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/usecase/solar-knowledge-management/backend/note_split/config.py b/usecase/solar-knowledge-management/backend/note_split/config.py
new file mode 100644
index 0000000..a7fe8d7
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_split/config.py
@@ -0,0 +1,90 @@
+"""Configuration management for the application."""
+import os
+from pathlib import Path
+from typing import Optional
+from dotenv import load_dotenv
+
+# Load environment variables from .env file
+load_dotenv()
+
+
+def _get_project_root() -> Path:
+ """Get the project root directory.
+
+ This function finds the project root by looking for pyproject.toml
+ starting from the current file's directory and going up.
+
+ Returns:
+ Path to the project root directory
+ """
+ current = Path(__file__).resolve()
+ # Start from backend/note_split/config.py, go up to project root
+ # Try going up 3 levels first (most common case)
+ for level in range(4):
+ candidate = current.parents[level]
+ if (candidate / 'pyproject.toml').exists():
+ return candidate
+ # Fallback: assume project root is 3 levels up from config.py
+ return current.parent.parent.parent.parent
+
+
+PROJECT_ROOT = _get_project_root()
+
+
+class Config:
+ """Application configuration manager."""
+
+ # Upstage API settings
+ UPSTAGE_API_KEY: str = os.getenv('UPSTAGE_API_KEY', '')
+ UPSTAGE_API_BASE: str = os.getenv('UPSTAGE_API_BASE', 'https://api.upstage.ai/v1/solar')
+ MODEL_NAME: str = os.getenv('MODEL_NAME', 'solar-pro')
+
+ # Application settings
+ DEFAULT_WORKSPACE: Path = Path(os.getenv('DEFAULT_WORKSPACE', './workspace'))
+ PROMPTS_DIR: Path = Path(os.getenv('PROMPTS_DIR', str(PROJECT_ROOT / 'prompts')))
+
+ # LLM settings
+ DEFAULT_TEMPERATURE: float = float(os.getenv('DEFAULT_TEMPERATURE', '0.7'))
+ DEFAULT_MAX_TOKENS: int = int(os.getenv('DEFAULT_MAX_TOKENS', '4096'))
+
+ # HTTP client timeout settings (in seconds)
+ HTTP_TIMEOUT_ASYNC: float = float(os.getenv('HTTP_TIMEOUT_ASYNC', '60.0'))
+ HTTP_TIMEOUT_SYNC: float = float(os.getenv('HTTP_TIMEOUT_SYNC', '120.0'))
+
+ # File settings
+ ATOMIC_NOTES_SUFFIX: str = os.getenv('ATOMIC_NOTES_SUFFIX', '-atoms')
+
+ # Atomic note generation settings
+ DEFAULT_ATOM_DIRECTION: str = os.getenv('DEFAULT_ATOM_DIRECTION', '')
+
+ @classmethod
+ def validate(cls) -> bool:
+ """Validate that required configuration is present.
+
+ Returns:
+ True if configuration is valid, False otherwise
+ """
+ if not cls.UPSTAGE_API_KEY:
+ return False
+ return True
+
+ @classmethod
+ def get_atomic_notes_folder(cls, raw_note_path: Path) -> Path:
+ """Generate the folder path for atomic notes based on raw note path.
+
+ Args:
+ raw_note_path: Path to the raw note file
+
+ Returns:
+ Path to the atomic notes folder
+ """
+ parent = raw_note_path.parent
+ stem = raw_note_path.stem
+ return parent / f"{stem}{cls.ATOMIC_NOTES_SUFFIX}"
+
+ @classmethod
+ def ensure_directories(cls):
+ """Ensure required directories exist."""
+ cls.DEFAULT_WORKSPACE.mkdir(parents=True, exist_ok=True)
+ cls.PROMPTS_DIR.mkdir(parents=True, exist_ok=True)
+
diff --git a/usecase/solar-knowledge-management/backend/note_split/core/__init__.py b/usecase/solar-knowledge-management/backend/note_split/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/usecase/solar-knowledge-management/backend/note_split/core/file_handler.py b/usecase/solar-knowledge-management/backend/note_split/core/file_handler.py
new file mode 100644
index 0000000..68971c4
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_split/core/file_handler.py
@@ -0,0 +1,278 @@
+"""File handling operations for raw notes and atomic notes."""
+from pathlib import Path
+from typing import List, Optional, Tuple, Union
+from ..models import Topic
+from ..config import Config
+from .path_utils import resolve_path
+
+
+class FileHandler:
+ """Handler for file operations related to notes."""
+
+ @staticmethod
+ def _resolve_path(path: Union[str, Path]) -> Path:
+ """Resolve and normalize a path for the current environment.
+
+ Args:
+ path: Path string or Path object
+
+ Returns:
+ Resolved and normalized Path object
+ """
+ return resolve_path(path)
+
+ @staticmethod
+ def read_note(note_path: Union[str, Path]) -> Tuple[Optional[str], Optional[List[str]]]:
+ """Read a markdown note file.
+
+ Args:
+ note_path: Path to the note file (string or Path)
+
+ Returns:
+ Tuple of (full content as string, list of lines) or (None, None) on error
+ """
+ try:
+ resolved_path = FileHandler._resolve_path(note_path)
+ with open(resolved_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+ lines = content.split('\n')
+ return content, lines
+ except FileNotFoundError:
+ print(f"File not found: {note_path}")
+ return None, None
+ except Exception as e:
+ print(f"Error reading file {note_path}: {e}")
+ return None, None
+
+ @staticmethod
+ def get_lines_content(lines: List[str], line_numbers: List[int]) -> str:
+ """Extract content from specific line numbers.
+
+ Args:
+ lines: List of all lines in the document
+ line_numbers: List of line numbers to extract (1-indexed)
+
+ Returns:
+ Concatenated content of specified lines
+ """
+ content_lines = []
+ for line_num in sorted(line_numbers):
+ # Convert 1-indexed to 0-indexed
+ idx = line_num - 1
+ if 0 <= idx < len(lines):
+ content_lines.append(lines[idx])
+
+ return '\n'.join(content_lines)
+
+ @staticmethod
+ def insert_backlinks(
+ lines: List[str],
+ topics: List[Topic]
+ ) -> List[str]:
+ """Insert backlinks to atomic notes in the raw note.
+
+ Backlinks are inserted at the last line of consecutive line ranges
+ to improve readability.
+
+ Args:
+ lines: List of lines from the raw note
+ topics: List of Topic objects with line numbers
+
+ Returns:
+ Modified list of lines with backlinks inserted
+ """
+ # Create a mapping of line numbers to topics
+ line_to_backlinks = {}
+
+ for topic in topics:
+ if not topic.line_numbers:
+ continue
+
+ # Group consecutive line numbers
+ sorted_lines = sorted(topic.line_numbers)
+ ranges = []
+ current_range = [sorted_lines[0]]
+
+ for line_num in sorted_lines[1:]:
+ if line_num == current_range[-1] + 1:
+ current_range.append(line_num)
+ else:
+ ranges.append(current_range)
+ current_range = [line_num]
+ ranges.append(current_range)
+
+ # Add backlink to the last line of each range
+ for line_range in ranges:
+ last_line = line_range[-1]
+ if last_line not in line_to_backlinks:
+ line_to_backlinks[last_line] = []
+ line_to_backlinks[last_line].append(f"[[{topic.topic}|{topics.index(topic) + 1}]]")
+
+ # Insert backlinks into lines
+ modified_lines = []
+ for i, line in enumerate(lines, 1):
+ modified_lines.append(line)
+ if i in line_to_backlinks:
+ # Add backlinks at the end of the line
+ backlinks_str = ' '.join(line_to_backlinks[i])
+ modified_lines[-1] = f"{line} {backlinks_str}"
+
+ return modified_lines
+
+ @staticmethod
+ def append_topic_list(
+ lines: List[str],
+ topics: List[Topic]
+ ) -> List[str]:
+ """Append list of generated atomic notes to the end of raw note.
+
+ Args:
+ lines: List of lines from the raw note
+ topics: List of Topic objects
+
+ Returns:
+ Modified list of lines with topic list appended
+ """
+ # Add separator
+ lines.append('')
+ lines.append('---')
+ lines.append('&&&')
+ lines.append('')
+ lines.append('## Generated Atomic Notes')
+ lines.append('')
+
+ # Add topic links
+ for topic in topics:
+ lines.append(f"{topics.index(topic) + 1}. [[{topic.topic}]]")
+
+ return lines
+
+ @staticmethod
+ def create_atomic_note(
+ topic: Topic,
+ related_content: str,
+ generated_content: Optional[str] = None
+ ) -> str:
+ """Create content for an atomic note.
+
+ Args:
+ topic: Topic object with metadata
+ related_content: Content extracted from the raw note
+ generated_content: Optional LLM-generated content for the note
+
+ Returns:
+ Complete markdown content for the atomic note
+ """
+ parts = []
+
+ # Add properties
+ parts.append(topic.get_properties_markdown())
+ parts.append('')
+
+ # Add title
+ parts.append(f"# {topic.topic}")
+ parts.append('')
+
+ # Add overview
+ parts.append("## Overview")
+ parts.append(topic.coverage)
+ parts.append('')
+
+ # Add related content from raw note
+ if related_content:
+ parts.append("## Related Content from Raw Note")
+ parts.append('')
+ parts.append(related_content)
+ parts.append('')
+
+ # Add generated content if available
+ if generated_content:
+ parts.append("## Generated Content")
+ parts.append('')
+ parts.append(generated_content)
+ parts.append('')
+
+ # Add keywords section
+ if topic.keywords:
+ parts.append("## Keywords")
+ parts.append('')
+ parts.append(', '.join(f"`{kw}`" for kw in topic.keywords))
+ parts.append('')
+
+ return '\n'.join(parts)
+
+ @staticmethod
+ def save_atomic_note(
+ save_folder: Union[str, Path],
+ topic: Topic,
+ content: str
+ ) -> bool:
+ """Save an atomic note to a file.
+
+ Args:
+ save_folder: Folder to save the atomic note (string or Path)
+ topic: Topic object
+ content: Markdown content for the note
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ resolved_folder = FileHandler._resolve_path(save_folder)
+ # Ensure save folder exists
+ resolved_folder.mkdir(parents=True, exist_ok=True)
+
+ # Sanitize filename
+ safe_filename = FileHandler._sanitize_filename(topic.topic)
+ file_path = resolved_folder / f"{safe_filename}.md"
+
+ with open(file_path, 'w', encoding='utf-8') as f:
+ f.write(content)
+ return True
+ except Exception as e:
+ print(f"Error saving atomic note: {e}")
+ return False
+
+ @staticmethod
+ def save_raw_note(note_path: Union[str, Path], lines: List[str]) -> bool:
+ """Save the modified raw note.
+
+ Args:
+ note_path: Path to the raw note file (string or Path)
+ lines: List of lines to write
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ resolved_path = FileHandler._resolve_path(note_path)
+ content = '\n'.join(lines)
+ with open(resolved_path, 'w', encoding='utf-8') as f:
+ f.write(content)
+ return True
+ except Exception as e:
+ print(f"Error saving raw note {note_path}: {e}")
+ return False
+
+ @staticmethod
+ def _sanitize_filename(filename: str) -> str:
+ """Sanitize a string to be used as a filename.
+
+ Args:
+ filename: String to sanitize
+
+ Returns:
+ Sanitized filename
+ """
+ # Replace invalid characters with underscore
+ invalid_chars = '<>:"/\\|?*'
+ for char in invalid_chars:
+ filename = filename.replace(char, '_')
+
+ # Limit length
+ max_length = 200
+ if len(filename) > max_length:
+ filename = filename[:max_length]
+
+ return filename.strip()
+
diff --git a/usecase/solar-knowledge-management/backend/note_split/core/path_utils.py b/usecase/solar-knowledge-management/backend/note_split/core/path_utils.py
new file mode 100644
index 0000000..62c85d5
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_split/core/path_utils.py
@@ -0,0 +1,110 @@
+"""Path utilities for cross-platform path handling, including WSL support."""
+import os
+import platform
+import re
+from pathlib import Path
+from typing import Union
+
+# Pattern to match Windows paths like C:\Users\... or C:/Users/...
+_WINDOWS_PATH_PATTERN = re.compile(r'^([A-Za-z]):[/\\](.*)$')
+
+
+def _is_wsl_environment() -> bool:
+ """Detect if running in WSL environment.
+
+ Returns:
+ True if running in WSL, False otherwise
+ """
+ if platform.system() != "Linux":
+ return False
+
+ # Check /proc/version for WSL indicators
+ if os.path.exists("/proc/version"):
+ try:
+ with open("/proc/version", "r", encoding="utf-8") as f:
+ version_info = f.read().lower()
+ if "microsoft" in version_info or "wsl" in version_info:
+ return True
+ except (OSError, IOError):
+ pass
+
+ # Check for /mnt/c mount point
+ if os.path.exists("/mnt/c"):
+ return True
+
+ return False
+
+
+def normalize_path_for_wsl(path: Union[str, Path]) -> Path:
+ """Normalize Windows paths to WSL paths when running in WSL environment.
+
+ Args:
+ path: Path string or Path object (can be Windows or WSL format)
+
+ Returns:
+ Normalized Path object suitable for the current environment
+ """
+ path_str = str(path).strip()
+
+ # Strip surrounding quotes (both single and double)
+ if (path_str.startswith('"') and path_str.endswith('"')) or \
+ (path_str.startswith("'") and path_str.endswith("'")):
+ path_str = path_str[1:-1].strip()
+
+ if not _is_wsl_environment():
+ return Path(path_str)
+
+ # Match Windows path pattern (C:\Users\... or C:/Users/...)
+ match = _WINDOWS_PATH_PATTERN.match(path_str)
+ if match:
+ drive_letter = match.group(1).lower()
+ rest_of_path = match.group(2).replace("\\", "/")
+ wsl_path = Path(f"/mnt/{drive_letter}/{rest_of_path}")
+ return wsl_path
+
+ return Path(path_str)
+
+
+def resolve_path(path: Union[str, Path]) -> Path:
+ """Resolve and normalize a path for the current environment.
+
+ This function:
+ - Converts Windows paths to WSL paths if in WSL
+ - Expands user home directory (~)
+ - Resolves to absolute path
+
+ Args:
+ path: Path string or Path object
+
+ Returns:
+ Resolved and normalized Path object
+ """
+ normalized = normalize_path_for_wsl(path)
+ return normalized.expanduser().resolve()
+
+
+def format_path_for_display(path: Union[str, Path], *, prefer_windows_format: bool = False) -> str:
+ r"""Format a path for display, optionally converting WSL paths to Windows format.
+
+ Args:
+ path: Path string or Path object
+ prefer_windows_format: If True and in WSL, convert /mnt/... to C:\... format
+
+ Returns:
+ Formatted path string
+ """
+ path_obj = Path(path)
+
+ if prefer_windows_format and _is_wsl_environment():
+ path_str = str(path_obj)
+ if path_str.startswith("/mnt/"):
+ parts = path_str.split("/", 3)
+ if len(parts) >= 4:
+ drive_letter = parts[2].upper()
+ rest_path = parts[3].replace("/", "\\")
+ return f"{drive_letter}:\\{rest_path}"
+
+ return str(path_obj)
+
+
+
diff --git a/usecase/solar-knowledge-management/backend/note_split/core/state_manager.py b/usecase/solar-knowledge-management/backend/note_split/core/state_manager.py
new file mode 100644
index 0000000..04a071a
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_split/core/state_manager.py
@@ -0,0 +1,206 @@
+"""State management for Streamlit session state."""
+from typing import List, Optional, Dict, Any
+import streamlit as st
+from pathlib import Path
+from ..models import Topic
+from .path_utils import resolve_path
+
+
+class StateManager:
+ """Manager for Streamlit session state."""
+
+ # State keys
+ KEY_RAW_NOTE_PATH = 'raw_note_path'
+ KEY_SAVE_FOLDER_PATH = 'save_folder_path'
+ KEY_RAW_NOTE_CONTENT = 'raw_note_content'
+ KEY_RAW_NOTE_LINES = 'raw_note_lines'
+ KEY_TOPICS = 'topics'
+ KEY_SELECTED_TEMPLATE = 'selected_template'
+ KEY_ANALYSIS_INSTRUCTIONS = 'analysis_instructions'
+ KEY_STEP = 'current_step'
+
+ # Step identifiers
+ STEP_INIT = 'init'
+ STEP_TEMPLATE_SELECT = 'template_select'
+ STEP_TOPICS_EXTRACTED = 'topics_extracted'
+ STEP_NOTES_GENERATED = 'notes_generated'
+
+ @staticmethod
+ def initialize():
+ """Initialize session state with default values."""
+ if StateManager.KEY_STEP not in st.session_state:
+ st.session_state[StateManager.KEY_STEP] = StateManager.STEP_INIT
+
+ if StateManager.KEY_RAW_NOTE_PATH not in st.session_state:
+ st.session_state[StateManager.KEY_RAW_NOTE_PATH] = None
+
+ if StateManager.KEY_SAVE_FOLDER_PATH not in st.session_state:
+ st.session_state[StateManager.KEY_SAVE_FOLDER_PATH] = None
+
+ if StateManager.KEY_RAW_NOTE_CONTENT not in st.session_state:
+ st.session_state[StateManager.KEY_RAW_NOTE_CONTENT] = None
+
+ if StateManager.KEY_RAW_NOTE_LINES not in st.session_state:
+ st.session_state[StateManager.KEY_RAW_NOTE_LINES] = None
+
+ if StateManager.KEY_TOPICS not in st.session_state:
+ st.session_state[StateManager.KEY_TOPICS] = []
+
+ if StateManager.KEY_SELECTED_TEMPLATE not in st.session_state:
+ st.session_state[StateManager.KEY_SELECTED_TEMPLATE] = None
+
+ if StateManager.KEY_ANALYSIS_INSTRUCTIONS not in st.session_state:
+ st.session_state[StateManager.KEY_ANALYSIS_INSTRUCTIONS] = ''
+
+ @staticmethod
+ def get_current_step() -> str:
+ """Get the current step in the workflow."""
+ return st.session_state.get(StateManager.KEY_STEP, StateManager.STEP_INIT)
+
+ @staticmethod
+ def set_step(step: str):
+ """Set the current workflow step."""
+ st.session_state[StateManager.KEY_STEP] = step
+
+ @staticmethod
+ def get_raw_note_path() -> Optional[Path]:
+ """Get the raw note path."""
+ path = st.session_state.get(StateManager.KEY_RAW_NOTE_PATH)
+ if path:
+ return resolve_path(path)
+ return None
+
+ @staticmethod
+ def set_raw_note_path(path: Path):
+ """Set the raw note path."""
+ st.session_state[StateManager.KEY_RAW_NOTE_PATH] = str(path)
+
+ @staticmethod
+ def get_save_folder_path() -> Optional[Path]:
+ """Get the save folder path."""
+ path = st.session_state.get(StateManager.KEY_SAVE_FOLDER_PATH)
+ if path:
+ return resolve_path(path)
+ return None
+
+ @staticmethod
+ def set_save_folder_path(path: Path):
+ """Set the save folder path."""
+ st.session_state[StateManager.KEY_SAVE_FOLDER_PATH] = str(path)
+
+ @staticmethod
+ def get_raw_note_content() -> Optional[str]:
+ """Get the raw note content."""
+ return st.session_state.get(StateManager.KEY_RAW_NOTE_CONTENT)
+
+ @staticmethod
+ def set_raw_note_content(content: str, lines: List[str]):
+ """Set the raw note content and lines."""
+ st.session_state[StateManager.KEY_RAW_NOTE_CONTENT] = content
+ st.session_state[StateManager.KEY_RAW_NOTE_LINES] = lines
+
+ @staticmethod
+ def get_raw_note_lines() -> Optional[List[str]]:
+ """Get the raw note lines."""
+ return st.session_state.get(StateManager.KEY_RAW_NOTE_LINES)
+
+ @staticmethod
+ def get_topics() -> List[Topic]:
+ """Get the list of topics."""
+ topics_data = st.session_state.get(StateManager.KEY_TOPICS, [])
+ return [Topic.from_dict(t) if isinstance(t, dict) else t for t in topics_data]
+
+ @staticmethod
+ def set_topics(topics: List[Topic]):
+ """Set the list of topics."""
+ st.session_state[StateManager.KEY_TOPICS] = [
+ t.to_dict() if isinstance(t, Topic) else t for t in topics
+ ]
+
+ @staticmethod
+ def add_topic(topic: Topic):
+ """Add a new topic to the list."""
+ topics = StateManager.get_topics()
+ topics.append(topic)
+ StateManager.set_topics(topics)
+
+ @staticmethod
+ def update_topic(index: int, topic: Topic):
+ """Update a topic at a specific index."""
+ topics = StateManager.get_topics()
+ if 0 <= index < len(topics):
+ topics[index] = topic
+ StateManager.set_topics(topics)
+
+ @staticmethod
+ def delete_topic(index: int):
+ """Delete a topic at a specific index."""
+ topics = StateManager.get_topics()
+ if 0 <= index < len(topics):
+ topics.pop(index)
+ StateManager.set_topics(topics)
+
+ @staticmethod
+ def delete_topics(indices: List[int]):
+ """Delete multiple topics by indices."""
+ topics = StateManager.get_topics()
+ # Sort indices in reverse to avoid index shifting issues
+ for index in sorted(indices, reverse=True):
+ if 0 <= index < len(topics):
+ topics.pop(index)
+ StateManager.set_topics(topics)
+
+ @staticmethod
+ def get_selected_topics() -> List[Topic]:
+ """Get list of topics that are selected."""
+ return [t for t in StateManager.get_topics() if t.selected]
+
+ @staticmethod
+ def get_selected_topic_indices() -> List[int]:
+ """Get indices of selected topics."""
+ topics = StateManager.get_topics()
+ return [i for i, t in enumerate(topics) if t.selected]
+
+ @staticmethod
+ def select_all_topics():
+ """Select all topics."""
+ topics = StateManager.get_topics()
+ for topic in topics:
+ topic.selected = True
+ StateManager.set_topics(topics)
+
+ @staticmethod
+ def deselect_all_topics():
+ """Deselect all topics."""
+ topics = StateManager.get_topics()
+ for topic in topics:
+ topic.selected = False
+ StateManager.set_topics(topics)
+
+ @staticmethod
+ def get_selected_template() -> Optional[str]:
+ """Get the selected prompt template name."""
+ return st.session_state.get(StateManager.KEY_SELECTED_TEMPLATE)
+
+ @staticmethod
+ def set_selected_template(template_name: str):
+ """Set the selected prompt template."""
+ st.session_state[StateManager.KEY_SELECTED_TEMPLATE] = template_name
+
+ @staticmethod
+ def get_analysis_instructions() -> str:
+ """Get the analysis instructions."""
+ return st.session_state.get(StateManager.KEY_ANALYSIS_INSTRUCTIONS, '')
+
+ @staticmethod
+ def set_analysis_instructions(instructions: str):
+ """Set the analysis instructions."""
+ st.session_state[StateManager.KEY_ANALYSIS_INSTRUCTIONS] = instructions
+
+ @staticmethod
+ def reset():
+ """Reset all state to initial values."""
+ for key in list(st.session_state.keys()):
+ del st.session_state[key]
+ StateManager.initialize()
+
diff --git a/usecase/solar-knowledge-management/backend/note_split/llm/__init__.py b/usecase/solar-knowledge-management/backend/note_split/llm/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/usecase/solar-knowledge-management/backend/note_split/llm/client.py b/usecase/solar-knowledge-management/backend/note_split/llm/client.py
new file mode 100644
index 0000000..e79f6e4
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_split/llm/client.py
@@ -0,0 +1,198 @@
+"""LLM client for Upstage API interaction."""
+import asyncio
+from typing import List, Optional, Dict, Any
+import httpx
+from ..config import Config
+from ..models import PromptTemplate
+
+
+class UpstageClient:
+ """Client for interacting with Upstage Solar API."""
+
+ def __init__(
+ self,
+ api_key: Optional[str] = None,
+ api_base: Optional[str] = None,
+ model: Optional[str] = None
+ ):
+ """Initialize the Upstage client.
+
+ Args:
+ api_key: Upstage API key (defaults to Config.UPSTAGE_API_KEY)
+ api_base: API base URL (defaults to Config.UPSTAGE_API_BASE)
+ model: Model name (defaults to Config.MODEL_NAME)
+ """
+ self.api_key = api_key or Config.UPSTAGE_API_KEY
+ self.api_base = api_base or Config.UPSTAGE_API_BASE
+ self.model = model or Config.MODEL_NAME
+
+ if not self.api_key:
+ raise ValueError("UPSTAGE_API_KEY is required")
+
+ def _get_headers(self) -> Dict[str, str]:
+ """Get request headers with API key."""
+ return {
+ "Authorization": f"Bearer {self.api_key}",
+ "Content-Type": "application/json"
+ }
+
+ async def _make_request(
+ self,
+ messages: List[Dict[str, str]],
+ temperature: float = Config.DEFAULT_TEMPERATURE,
+ max_tokens: int = Config.DEFAULT_MAX_TOKENS
+ ) -> Optional[str]:
+ """Make an async request to the Upstage API.
+
+ Args:
+ messages: List of message dictionaries with 'role' and 'content'
+ temperature: Sampling temperature
+ max_tokens: Maximum tokens to generate
+
+ Returns:
+ Generated text or None on error
+ """
+ url = f"{self.api_base}/chat/completions"
+ payload = {
+ "model": self.model,
+ "messages": messages,
+ "temperature": temperature,
+ "max_tokens": max_tokens
+ }
+
+ try:
+ async with httpx.AsyncClient(timeout=Config.HTTP_TIMEOUT_ASYNC) as client:
+ response = await client.post(
+ url,
+ headers=self._get_headers(),
+ json=payload
+ )
+ response.raise_for_status()
+
+ data = response.json()
+ return data['choices'][0]['message']['content']
+ except httpx.HTTPStatusError as e:
+ print(f"HTTP error occurred: {e}")
+ return None
+ except Exception as e:
+ print(f"Error making request: {e}")
+ return None
+
+ def make_request_sync(
+ self,
+ messages: List[Dict[str, str]],
+ temperature: float = Config.DEFAULT_TEMPERATURE,
+ max_tokens: int = Config.DEFAULT_MAX_TOKENS
+ ) -> Optional[str]:
+ """Make a synchronous request to the Upstage API.
+
+ Args:
+ messages: List of message dictionaries with 'role' and 'content'
+ temperature: Sampling temperature
+ max_tokens: Maximum tokens to generate
+
+ Returns:
+ Generated text or None on error
+ """
+ url = f"{self.api_base}/chat/completions"
+ payload = {
+ "model": self.model,
+ "messages": messages,
+ "temperature": temperature,
+ "max_tokens": max_tokens
+ }
+
+ try:
+ with httpx.Client(timeout=Config.HTTP_TIMEOUT_SYNC) as client:
+ response = client.post(
+ url,
+ headers=self._get_headers(),
+ json=payload
+ )
+ response.raise_for_status()
+
+ data = response.json()
+ return data['choices'][0]['message']['content']
+ except httpx.HTTPStatusError as e:
+ print(f"HTTP error occurred: {e}")
+ return None
+ except Exception as e:
+ print(f"Error making request: {e}")
+ return None
+
+ async def generate_with_template(
+ self,
+ template: PromptTemplate,
+ user_vars: Dict[str, Any],
+ temperature: float = Config.DEFAULT_TEMPERATURE,
+ max_tokens: int = Config.DEFAULT_MAX_TOKENS
+ ) -> Optional[str]:
+ """Generate text using a prompt template.
+
+ Args:
+ template: PromptTemplate object
+ user_vars: Variables to format the user prompt
+ temperature: Sampling temperature
+ max_tokens: Maximum tokens to generate
+
+ Returns:
+ Generated text or None on error
+ """
+ messages = [
+ {"role": "system", "content": template.system_prompt},
+ {"role": "user", "content": template.format_user_prompt(**user_vars)}
+ ]
+
+ return await self._make_request(messages, temperature, max_tokens)
+
+ def generate_with_template_sync(
+ self,
+ template: PromptTemplate,
+ user_vars: Dict[str, Any],
+ temperature: float = Config.DEFAULT_TEMPERATURE,
+ max_tokens: int = Config.DEFAULT_MAX_TOKENS
+ ) -> Optional[str]:
+ """Generate text using a prompt template (synchronous).
+
+ Args:
+ template: PromptTemplate object
+ user_vars: Variables to format the user prompt
+ temperature: Sampling temperature
+ max_tokens: Maximum tokens to generate
+
+ Returns:
+ Generated text or None on error
+ """
+ messages = [
+ {"role": "system", "content": template.system_prompt},
+ {"role": "user", "content": template.format_user_prompt(**user_vars)}
+ ]
+
+ return self.make_request_sync(messages, temperature, max_tokens)
+
+ async def generate_batch(
+ self,
+ requests: List[Dict[str, Any]]
+ ) -> List[Optional[str]]:
+ """Generate multiple responses in parallel.
+
+ Args:
+ requests: List of request dictionaries, each containing:
+ - messages: List of message dicts
+ - temperature (optional): Sampling temperature
+ - max_tokens (optional): Maximum tokens to generate
+
+ Returns:
+ List of generated texts (or None for failed requests)
+ """
+ tasks = []
+ for req in requests:
+ messages = req['messages']
+ temperature = req.get('temperature', Config.DEFAULT_TEMPERATURE)
+ max_tokens = req.get('max_tokens', Config.DEFAULT_MAX_TOKENS)
+
+ task = self._make_request(messages, temperature, max_tokens)
+ tasks.append(task)
+
+ return await asyncio.gather(*tasks)
+
diff --git a/usecase/solar-knowledge-management/backend/note_split/llm/parsers.py b/usecase/solar-knowledge-management/backend/note_split/llm/parsers.py
new file mode 100644
index 0000000..ae326f7
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_split/llm/parsers.py
@@ -0,0 +1,143 @@
+"""Parsers for LLM responses."""
+import json
+import re
+from typing import List, Optional, Dict, Any
+from ..models import Topic
+
+
+class ResponseParser:
+ """Parser for LLM responses."""
+
+ @staticmethod
+ def parse_topics_from_json(response: str) -> List[Topic]:
+ """Parse topics from JSON response.
+
+ Expected JSON format:
+ {
+ "topics": [
+ {
+ "topic": "Topic Name",
+ "coverage": "Brief overview",
+ "line_numbers": [1, 2, 3],
+ "keywords": ["keyword1", "keyword2"]
+ }
+ ]
+ }
+
+ Args:
+ response: JSON string response from LLM
+
+ Returns:
+ List of Topic objects
+ """
+ try:
+ # Try to extract JSON from markdown code blocks if present
+ json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL)
+ if json_match:
+ response = json_match.group(1)
+
+ data = json.loads(response)
+ topics_data = data.get('topics', [])
+
+ topics = []
+ for topic_data in topics_data:
+ topic = Topic(
+ topic=topic_data.get('topic', ''),
+ coverage=topic_data.get('coverage', ''),
+ line_numbers=topic_data.get('line_numbers', []),
+ keywords=topic_data.get('keywords', [])
+ )
+ topics.append(topic)
+
+ return topics
+ except json.JSONDecodeError as e:
+ print(f"Error parsing JSON response: {e}")
+ return []
+ except Exception as e:
+ print(f"Error processing topics: {e}")
+ return []
+
+ @staticmethod
+ def parse_line_numbers_from_json(response: str) -> List[int]:
+ """Parse line numbers from JSON response.
+
+ Expected JSON format:
+ {
+ "line_numbers": [1, 2, 3, 5, 7]
+ }
+
+ Args:
+ response: JSON string response from LLM
+
+ Returns:
+ List of line numbers
+ """
+ try:
+ # Try to extract JSON from markdown code blocks if present
+ json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL)
+ if json_match:
+ response = json_match.group(1)
+
+ data = json.loads(response)
+ return data.get('line_numbers', [])
+ except json.JSONDecodeError as e:
+ print(f"Error parsing JSON response: {e}")
+ return []
+ except Exception as e:
+ print(f"Error processing line numbers: {e}")
+ return []
+
+ @staticmethod
+ def parse_atomic_note_content(response: str) -> str:
+ """Parse atomic note content from LLM response.
+
+ The response should contain markdown content for the atomic note.
+
+ Args:
+ response: Markdown content from LLM
+
+ Returns:
+ Cleaned atomic note content
+ """
+ # Remove any markdown code blocks if the entire response is wrapped
+ if response.strip().startswith('```') and response.strip().endswith('```'):
+ # Remove the first and last line (markdown fences)
+ lines = response.strip().split('\n')
+ if len(lines) > 2:
+ # Remove first line and last line
+ response = '\n'.join(lines[1:-1])
+
+ return response.strip()
+
+ @staticmethod
+ def extract_json_from_response(response: str) -> Optional[Dict[str, Any]]:
+ """Extract and parse JSON from a response that may contain other text.
+
+ Args:
+ response: Response text that may contain JSON
+
+ Returns:
+ Parsed JSON dictionary or None if parsing fails
+ """
+ # Try to find JSON in code blocks first
+ json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL)
+ if json_match:
+ try:
+ return json.loads(json_match.group(1))
+ except json.JSONDecodeError:
+ pass
+
+ # Try to find JSON object in the text
+ json_match = re.search(r'\{.*\}', response, re.DOTALL)
+ if json_match:
+ try:
+ return json.loads(json_match.group(0))
+ except json.JSONDecodeError:
+ pass
+
+ # Try parsing the entire response
+ try:
+ return json.loads(response)
+ except json.JSONDecodeError:
+ return None
+
diff --git a/usecase/solar-knowledge-management/backend/note_split/llm/prompt_loader.py b/usecase/solar-knowledge-management/backend/note_split/llm/prompt_loader.py
new file mode 100644
index 0000000..6bb8abf
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_split/llm/prompt_loader.py
@@ -0,0 +1,86 @@
+"""Prompt template loading functionality."""
+import yaml
+from pathlib import Path
+from typing import Dict, List, Optional
+from ..models import PromptTemplate
+from ..config import Config
+
+
+class PromptLoader:
+ """Loader for prompt templates from YAML files."""
+
+ def __init__(self, prompts_dir: Optional[Path] = None):
+ """Initialize the prompt loader.
+
+ Args:
+ prompts_dir: Directory containing prompt template files
+ """
+ self.prompts_dir = prompts_dir or Config.PROMPTS_DIR
+
+ def load_template(self, template_name: str) -> Optional[PromptTemplate]:
+ """Load a specific prompt template by name.
+
+ Args:
+ template_name: Name of the template file (without .yml extension)
+
+ Returns:
+ PromptTemplate object or None if not found
+ """
+ template_path = self.prompts_dir / f"{template_name}.yml"
+
+ if not template_path.exists():
+ return None
+
+ try:
+ with open(template_path, 'r', encoding='utf-8') as f:
+ data = yaml.safe_load(f)
+
+ # 빈 파일이나 None인 경우 명시적으로 처리
+ if data is None:
+ print(f"Warning: Template file '{template_name}.yml' is empty or contains no valid YAML content.")
+ return None
+
+ # data가 딕셔너리가 아닌 경우 처리
+ if not isinstance(data, dict):
+ print(f"Error loading template {template_name}: YAML content must be a dictionary, got {type(data).__name__}")
+ return None
+
+ return PromptTemplate(
+ name=data.get('name', template_name),
+ description=data.get('description', ''),
+ system_prompt=data.get('system_prompt', ''),
+ user_prompt_template=data.get('user_prompt_template', '')
+ )
+ except yaml.YAMLError as e:
+ print(f"Error parsing YAML in template {template_name}: {e}")
+ return None
+ except Exception as e:
+ print(f"Error loading template {template_name}: {e}")
+ return None
+
+ def list_templates(self) -> List[str]:
+ """List all available template names.
+
+ Returns:
+ List of template names (without .yml extension)
+ """
+ if not self.prompts_dir.exists():
+ return []
+
+ return [
+ p.stem for p in self.prompts_dir.glob('*.yml')
+ ]
+
+ def get_templates_info(self) -> Dict[str, str]:
+ """Get information about all available templates.
+
+ Returns:
+ Dictionary mapping template names to their descriptions
+ """
+ templates_info = {}
+ for template_name in self.list_templates():
+ template = self.load_template(template_name)
+ if template:
+ templates_info[template_name] = template.description
+ return templates_info
+
diff --git a/usecase/solar-knowledge-management/backend/note_split/models.py b/usecase/solar-knowledge-management/backend/note_split/models.py
new file mode 100644
index 0000000..fbe2408
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_split/models.py
@@ -0,0 +1,85 @@
+"""Data models for the atomic notes application."""
+from dataclasses import dataclass, field
+from typing import List, Optional
+
+
+@dataclass
+class Topic:
+ """Represents a topic extracted from a raw note.
+
+ Attributes:
+ topic: The name/title of the topic
+ coverage: Brief summary/overview of the topic
+ line_numbers: List of line numbers in the raw note related to this topic
+ keywords: List of key terms related to this topic
+ user_direction: Optional user instructions for generating the atomic note
+ use_llm: Whether to use LLM for generating atomic note content
+ selected: Whether this topic is selected for batch operations
+ """
+ topic: str
+ coverage: str
+ line_numbers: List[int] = field(default_factory=list)
+ keywords: List[str] = field(default_factory=list)
+ user_direction: Optional[str] = None
+ use_llm: bool = False
+ selected: bool = False
+
+ def to_dict(self) -> dict:
+ """Convert Topic to dictionary."""
+ return {
+ 'topic': self.topic,
+ 'coverage': self.coverage,
+ 'line_numbers': self.line_numbers,
+ 'keywords': self.keywords,
+ 'user_direction': self.user_direction,
+ 'use_llm': self.use_llm,
+ 'selected': self.selected
+ }
+
+ @classmethod
+ def from_dict(cls, data: dict) -> 'Topic':
+ """Create Topic from dictionary."""
+ return cls(
+ topic=data.get('topic', ''),
+ coverage=data.get('coverage', ''),
+ line_numbers=data.get('line_numbers', []),
+ keywords=data.get('keywords', []),
+ user_direction=data.get('user_direction'),
+ use_llm=data.get('use_llm', False),
+ selected=data.get('selected', False)
+ )
+
+ def get_properties_markdown(self) -> str:
+ """Generate markdown properties section for the atomic note."""
+ props = [
+ "---",
+ f"topic: {self.topic}",
+ f"coverage: {self.coverage}",
+ f"line_numbers: {self.line_numbers}",
+ f"keywords: {', '.join(self.keywords)}",
+ ]
+ if self.user_direction:
+ props.append(f"user_direction: {self.user_direction}")
+ props.append("---")
+ return '\n'.join(props)
+
+
+@dataclass
+class PromptTemplate:
+ """Represents a prompt template for LLM interaction.
+
+ Attributes:
+ name: Template identifier
+ description: Brief description of the template's purpose
+ system_prompt: System-level instructions for the LLM
+ user_prompt_template: Template for user prompt with placeholders
+ """
+ name: str
+ description: str
+ system_prompt: str
+ user_prompt_template: str
+
+ def format_user_prompt(self, **kwargs) -> str:
+ """Format the user prompt with given variables."""
+ return self.user_prompt_template.format(**kwargs)
+
diff --git a/usecase/solar-knowledge-management/backend/note_split/ui/__init__.py b/usecase/solar-knowledge-management/backend/note_split/ui/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/usecase/solar-knowledge-management/backend/note_split/ui/components.py b/usecase/solar-knowledge-management/backend/note_split/ui/components.py
new file mode 100644
index 0000000..f07b586
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/note_split/ui/components.py
@@ -0,0 +1,382 @@
+"""Reusable UI components for Streamlit application."""
+import streamlit as st
+from typing import List, Optional, Callable
+from ..models import Topic
+from ..core.state_manager import StateManager
+
+
+def render_topic_card(
+ topic: Topic,
+ index: int,
+ on_update: Optional[Callable] = None,
+ on_delete: Optional[Callable] = None,
+ on_select: Optional[Callable] = None
+):
+ """Render a topic card with information and actions.
+
+ Args:
+ topic: Topic object to display
+ index: Index of the topic in the list
+ on_update: Callback for update action
+ on_delete: Callback for delete action
+ on_select: Callback for selection toggle
+ """
+ with st.container():
+ # Create columns for layout
+ col_check, col_llm, col_content, col_actions = st.columns([0.5, 0.5, 7.5, 1.5])
+
+ # Checkbox for selection
+ with col_check:
+ if on_select:
+ st.markdown(
+ '✓',
+ unsafe_allow_html=True
+ )
+ selected = st.checkbox(
+ "선택",
+ value=topic.selected,
+ key=f"select_{index}",
+ label_visibility="collapsed",
+ help="원자 노트 생성을 위해 이 토픽을 선택합니다"
+ )
+ if selected != topic.selected:
+ on_select(index, selected)
+
+ # Checkbox for LLM generation
+ with col_llm:
+ st.markdown(
+ '🤖',
+ unsafe_allow_html=True
+ )
+ use_llm = st.checkbox(
+ "LLM 생성",
+ value=topic.use_llm,
+ key=f"use_llm_{index}",
+ label_visibility="collapsed",
+ help="LLM을 사용하여 원자 노트 초안과 작성 가이드라인을 생성합니다"
+ )
+ if use_llm != topic.use_llm:
+ # Update topic's use_llm field
+ updated_topic = Topic(
+ topic=topic.topic,
+ coverage=topic.coverage,
+ line_numbers=topic.line_numbers,
+ keywords=topic.keywords,
+ user_direction=topic.user_direction,
+ use_llm=use_llm,
+ selected=topic.selected
+ )
+ if on_update:
+ on_update(index, updated_topic)
+
+ # Main content
+ with col_content:
+ st.markdown(f"### {topic.topic}")
+ st.markdown(f"**Overview:** {topic.coverage}")
+
+ if topic.keywords:
+ keywords_str = ', '.join(f'`{kw}`' for kw in topic.keywords)
+ st.markdown(f"**Keywords:** {keywords_str}")
+
+ if topic.line_numbers:
+ lines_str = ', '.join(str(ln) for ln in sorted(topic.line_numbers))
+ st.markdown(f"**Related Lines:** {lines_str}")
+
+ # Action buttons
+ with col_actions:
+ col_edit, col_del = st.columns(2)
+
+ with col_edit:
+ if st.button("✏️", key=f"edit_{index}", help="Edit topic"):
+ st.session_state[f'editing_{index}'] = True
+
+ with col_del:
+ if st.button("🗑️", key=f"delete_{index}", help="Delete topic"):
+ if on_delete:
+ on_delete(index)
+
+ # Edit modal (using expander as a simple modal)
+ if st.session_state.get(f'editing_{index}', False):
+ with st.expander("Edit Topic", expanded=True):
+ render_topic_edit_form(topic, index, on_update)
+
+ st.divider()
+
+
+def render_topic_edit_form(
+ topic: Topic,
+ index: int,
+ on_save: Optional[Callable] = None
+):
+ """Render an edit form for a topic.
+
+ Args:
+ topic: Topic object to edit
+ index: Index of the topic
+ on_save: Callback when save is clicked
+ """
+ new_topic = st.text_input(
+ "Topic Name",
+ value=topic.topic,
+ key=f"edit_topic_{index}"
+ )
+
+ new_coverage = st.text_area(
+ "Coverage",
+ value=topic.coverage,
+ key=f"edit_coverage_{index}",
+ height=100
+ )
+
+ new_keywords = st.text_input(
+ "Keywords (comma-separated)",
+ value=', '.join(topic.keywords),
+ key=f"edit_keywords_{index}"
+ )
+
+ new_user_direction = st.text_area(
+ "User Direction (optional)",
+ value=topic.user_direction or '',
+ key=f"edit_direction_{index}",
+ height=80,
+ help="Additional instructions for generating the atomic note"
+ )
+
+ new_use_llm = st.checkbox(
+ "Use LLM for content generation",
+ value=topic.use_llm,
+ key=f"edit_use_llm_{index}",
+ help="Enable LLM to generate draft content and writing guidelines"
+ )
+
+ col_save, col_cancel = st.columns(2)
+
+ with col_save:
+ if st.button("Save", key=f"save_{index}"):
+ updated_topic = Topic(
+ topic=new_topic,
+ coverage=new_coverage,
+ line_numbers=topic.line_numbers,
+ keywords=[kw.strip() for kw in new_keywords.split(',') if kw.strip()],
+ user_direction=new_user_direction if new_user_direction else None,
+ use_llm=new_use_llm,
+ selected=topic.selected
+ )
+
+ if on_save:
+ on_save(index, updated_topic)
+
+ st.session_state[f'editing_{index}'] = False
+ st.rerun()
+
+ with col_cancel:
+ if st.button("Cancel", key=f"cancel_{index}"):
+ st.session_state[f'editing_{index}'] = False
+ st.rerun()
+
+
+def render_topics_list(
+ topics: List[Topic],
+ on_update: Optional[Callable] = None,
+ on_delete: Optional[Callable] = None,
+ on_select: Optional[Callable] = None
+):
+ """Render a list of topic cards.
+
+ Args:
+ topics: List of Topic objects
+ on_update: Callback for topic updates
+ on_delete: Callback for topic deletion
+ on_select: Callback for topic selection
+ """
+ if not topics:
+ st.info("No topics extracted yet. Please run topic extraction first.")
+ return
+
+ st.markdown(f"### Extracted Topics ({len(topics)})")
+ st.markdown("---")
+
+ for i, topic in enumerate(topics):
+ render_topic_card(topic, i, on_update, on_delete, on_select)
+
+
+def render_batch_actions():
+ """Render batch action buttons."""
+ col1, col2, col3 = st.columns([1, 1, 2])
+
+ with col1:
+ if st.button("Select All", use_container_width=True):
+ StateManager.select_all_topics()
+ st.rerun()
+
+ with col2:
+ if st.button("Deselect All", use_container_width=True):
+ StateManager.deselect_all_topics()
+ st.rerun()
+
+ with col3:
+ selected_indices = StateManager.get_selected_topic_indices()
+ if selected_indices:
+ if st.button(
+ f"Delete Selected ({len(selected_indices)})",
+ use_container_width=True,
+ type="primary"
+ ):
+ st.session_state['confirm_batch_delete'] = True
+
+ # Confirmation dialog
+ if st.session_state.get('confirm_batch_delete', False):
+ st.warning("Are you sure you want to delete the selected topics?")
+ col_yes, col_no = st.columns(2)
+
+ with col_yes:
+ if st.button("Yes, Delete", type="primary"):
+ selected_indices = StateManager.get_selected_topic_indices()
+ StateManager.delete_topics(selected_indices)
+ st.session_state['confirm_batch_delete'] = False
+ st.success(f"Deleted {len(selected_indices)} topic(s)")
+ st.rerun()
+
+ with col_no:
+ if st.button("Cancel"):
+ st.session_state['confirm_batch_delete'] = False
+ st.rerun()
+
+
+def render_add_topics_form(on_add: Optional[Callable] = None):
+ """Render form for adding new topics.
+
+ Args:
+ on_add: Callback when new topics should be added
+ """
+ with st.expander("Add New Topics"):
+ st.markdown("### Request Additional Topics")
+
+ guidance = st.text_area(
+ "Topic Generation Guidance",
+ placeholder="E.g., 'Focus on technical implementation details' or 'Extract business logic concepts'",
+ help="Provide guidance for what kind of topics to extract"
+ )
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ num_topics = st.number_input(
+ "Number of Topics",
+ min_value=1,
+ max_value=10,
+ value=3,
+ help="How many additional topics to generate"
+ )
+
+ with col2:
+ template = st.selectbox(
+ "Prompt Template",
+ options=["topic_extract_amend_default", "topic_extract_diverse"],
+ help="Select the prompt template for topic extraction"
+ )
+
+ if st.button("Generate New Topics", type="primary"):
+ if on_add:
+ on_add(guidance, num_topics, template)
+
+
+def render_progress_indicator(message: str, progress: Optional[float] = None):
+ """Render a progress indicator.
+
+ Args:
+ message: Message to display
+ progress: Optional progress value (0.0 to 1.0)
+ """
+ if progress is not None:
+ st.progress(progress, text=message)
+ else:
+ with st.spinner(message):
+ pass
+
+
+def render_file_input_section():
+ """Render the file input section."""
+ st.markdown("## 1. Initial Setup")
+
+ note_path = st.text_input(
+ "Raw Note Path",
+ placeholder="/path/to/your/note.md",
+ help="Path to the markdown note file you want to analyze"
+ )
+
+ save_folder = st.text_input(
+ "Save Folder Path (optional)",
+ placeholder="Leave empty to use default location",
+ help="Folder where atomic notes will be saved. If empty, defaults to [note-name]-atoms/"
+ )
+
+ analysis_instructions = st.text_area(
+ "Analysis Instructions (optional)",
+ placeholder="Additional instructions for topic extraction...",
+ help="Optional guidance for the LLM when extracting topics",
+ height=100
+ )
+
+ return note_path, save_folder, analysis_instructions
+
+
+def render_template_selection_section(templates: dict):
+ """Render the template selection section.
+
+ Args:
+ templates: Dictionary of template names to descriptions
+
+ Returns:
+ Selected template name or None
+ """
+ st.markdown("## 2. Select Prompt Template")
+
+ if not templates:
+ st.warning("No prompt templates found. Please add template files to the prompts/ folder.")
+ return None
+
+ # Filter templates: only include topic_extract_* templates that are NOT topic_extract_amend_*
+ filtered_templates = {
+ name: desc for name, desc in templates.items()
+ if name.startswith('topic_extract_') and not name.startswith('topic_extract_amend_')
+ }
+
+ if not filtered_templates:
+ st.warning("No valid topic extraction templates found.")
+ return None
+
+ # Create options with descriptions
+ template_options = {
+ name: f"{name} - {desc}" for name, desc in filtered_templates.items()
+ }
+
+ # Set default to topic_extract_default if it exists, otherwise use first template
+ default_index = 0
+ if 'topic_extract_default' in filtered_templates:
+ default_index = list(filtered_templates.keys()).index('topic_extract_default')
+
+ selected = st.selectbox(
+ "Choose a template",
+ options=list(template_options.keys()),
+ index=default_index,
+ format_func=lambda x: template_options[x]
+ )
+
+ return selected
+
+
+def render_error(message: str):
+ """Render an error message."""
+ st.error(f"❌ {message}")
+
+
+def render_success(message: str):
+ """Render a success message."""
+ st.success(f"✅ {message}")
+
+
+def render_info(message: str):
+ """Render an info message."""
+ st.info(f"ℹ️ {message}")
+
diff --git a/usecase/solar-knowledge-management/backend/related_note/__init__.py b/usecase/solar-knowledge-management/backend/related_note/__init__.py
new file mode 100644
index 0000000..6462262
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/related_note/__init__.py
@@ -0,0 +1 @@
+from .related_note import Related_Note
\ No newline at end of file
diff --git a/usecase/solar-knowledge-management/backend/related_note/related_note.py b/usecase/solar-knowledge-management/backend/related_note/related_note.py
new file mode 100644
index 0000000..62d493f
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/related_note/related_note.py
@@ -0,0 +1,346 @@
+from dotenv import load_dotenv
+from langchain_chroma import Chroma
+from langchain_upstage import UpstageEmbeddings
+from pathlib import Path
+import tiktoken
+import math
+from uuid import uuid4
+import os
+import re
+
+load_dotenv()
+
+
+class Related_Note:
+ """
+ Obsidian vault 안의 md 파일들을 임베딩하고,
+ 특정 노트에 대한 연관 노트 3개를 찾아 [[링크]]로 추가하는 클래스.
+
+ - UpstageEmbeddings + Chroma DB 사용
+ - embedded_markers.txt 로 "이미 임베딩된 파일"을 추적
+ """
+
+ def __init__(self, vault_path: str) -> None:
+ """
+ - vault_path / vector_store 경로 / embedded marker 경로 설정
+ - Upstage 임베딩 객체 생성
+ - Chroma 벡터스토어 로드 또는 새로 생성
+ - embedded_markers.txt 파일이 없으면 빈 파일 생성
+ * embedded_markers는 이미 임베딩된 노트들 기록해둠으로서 추후 중복 임베딩 및 저장 피하는 용도
+
+ Args:
+ vault_path (str): Obsidian Vault 디렉토리의 절대 경로
+ """
+ self.vault_path = Path(vault_path).resolve()
+ base_path = Path(__file__).resolve()
+ self.embedding_path = base_path.parent
+ self.store_dir = self.embedding_path / "vector_store"
+ self.marker_root = self.embedding_path / "embedded_markers.txt"
+
+ # 임베딩 / 토크나이저 설정
+ self.embeddings = UpstageEmbeddings(model="embedding-query")
+ # 업스테이지 임베딩 최대 가능 인풋인 4000토큰 측정
+ self.enc = tiktoken.encoding_for_model("text-embedding-3-small")
+
+ # Chroma 벡터스토어 초기화
+ self.vector_store = self._init_vector_store()
+
+ # 마커 파일 보장
+ self._ensure_marker_file()
+
+ def _init_vector_store(self) -> Chroma:
+ """
+ Chroma 벡터스토어를 초기화합니다.
+
+ Returns:
+ Chroma: persist_directory를 가지는 Chroma 인스턴스.
+ """
+ if self.store_dir.exists():
+ return Chroma(
+ persist_directory=str(self.store_dir),
+ embedding_function=self.embeddings,
+ )
+ else:
+ # Chroma.from_texts 를 쓰기 위해 dummy 데이터 한 번 넣었다가 바로 삭제
+ random_id = str(uuid4())
+ vs = Chroma.from_texts(
+ texts=["dummy_data"],
+ ids=[random_id],
+ embedding=self.embeddings,
+ persist_directory=str(self.store_dir),
+ )
+ vs.delete(ids=[random_id])
+ return vs
+
+ def _ensure_marker_file(self) -> None:
+ """
+ embedded_markers.txt 파일이 없으면 빈 파일로 생성합니다.
+ Args:
+ None
+ Returns:
+ None
+ """
+ if not self.marker_root.exists():
+ self.marker_root.write_text("", encoding="utf-8")
+
+ # ──────────────────────────────────────────────────────────
+ # 마커(이미 임베딩된 노트) 관련
+ # ──────────────────────────────────────────────────────────
+ def load_embedded_notes(self) -> set[str]:
+ """
+ 이미 임베딩된 노트들의 상대 경로 집합을 읽어옵니다.
+ Args:
+ None
+ Returns:
+ set[str]: vault 기준 경로 집합.
+ """
+ if not self.marker_root.exists():
+ return set()
+
+ lines = self.marker_root.read_text(encoding="utf-8").splitlines()
+ return {line.strip() for line in lines if line.strip()}
+
+ def save_embedded_notes(self, new_notes: list[str]) -> None:
+ """
+ 새로 임베딩된 노트들의 상대 경로를 마커 파일에 추가합니다.
+ Args:
+ new_notes (list[str]): vault 기준 상대 경로 리스트.
+ Returns:
+ None
+ """
+ with self.marker_root.open("a", encoding="utf-8") as f:
+ for rel in new_notes:
+ f.write(rel + "\n")
+
+ # ──────────────────────────────────────────────────────────
+ # 임베딩 대상 선택 / 청킹 / 전처리
+ # ──────────────────────────────────────────────────────────
+ def get_unembedded_notes(self) -> list[str]:
+ """
+ 아직 임베딩되지 않은 md 파일 경로들을 찾습니다.
+ - embedded_markers.txt에 없는 파일만 대상
+ - 'upthink' 디렉토리 하위는 제외
+ - 경로는 vault 기준 상대 경로로 반환
+
+ Args:
+ None
+ Returns:
+ list[str]: 아직 임베딩되지 않은 md 파일들의 상대 경로 리스트.
+
+ * frontend의 경우, to_embed 리스트 내 파일을 임베딩하겠다는 메시지를 보여주면 될 것 같습니다.
+ """
+ embedded = self.load_embedded_notes()
+ to_embed: list[str] = []
+
+ for md_file in self.vault_path.rglob("*.md"):
+ rel = md_file.relative_to(self.vault_path).as_posix()
+ if ".venv" in rel.split("/"):
+ continue
+ if rel not in embedded:
+ to_embed.append(rel)
+
+ return to_embed
+
+ def chunk_text(self, text: str) -> list[str]:
+ """
+ 텍스트를 최대 약 4000 토큰 단위로 청킹합니다.
+ Args:
+ text (str): 원본 텍스트.
+ Returns:
+ list[str]: 청크 텍스트 리스트. (4000 토큰 이하이면 길이 1 리스트)
+ """
+ token_list = self.enc.encode(text)
+ total_tokens = len(token_list)
+ chunks: list[str] = []
+
+ if total_tokens > 4000:
+ n_chunks = math.ceil(total_tokens / 4000)
+ chunk_size = math.ceil(total_tokens / n_chunks)
+ for i in range(0, total_tokens, chunk_size):
+ chunk_tokens = token_list[i : i + chunk_size]
+ chunk_text = self.enc.decode(chunk_tokens)
+ chunks.append(chunk_text)
+ else:
+ chunks.append(text)
+
+ return chunks
+
+ def clean_text(self, text: str) -> str:
+ """
+ md 파일 내용을 간단히 전처리합니다.
+ - Windows/Unix 줄바꿈 정규화 (Windows 호환성)
+ - 특수 공백 문자 제거 (non-breaking space 등)
+ - 굵게(**) 마크다운 제거
+ - 연속 개행을 하나로 축소
+ - 앞뒤 공백 제거
+
+ Args:
+ text (str): 원본 텍스트.
+ Returns:
+ str: 전처리된 텍스트.
+ """
+ # Windows 줄바꿈(\r\n)을 Unix 스타일(\n)로 정규화
+ x = text.replace("\r\n", "\n").replace("\r", "\n")
+ x = re.sub(r"[\xa0\u200b]", "", x)
+ x = re.sub(r"\*\*", "", x)
+ x = re.sub(r"\n+", "\n", x)
+ x = x.strip()
+ return x
+
+ # ──────────────────────────────────────────────────────────
+ # 임베딩 실행
+ # ──────────────────────────────────────────────────────────
+ def index_unembedded_notes(self) -> None:
+ """
+ 아직 임베딩되지 않은 md 파일들을 찾아 모두 임베딩합니다.
+ - get_unembedded_notes()로 대상 탐색
+ - clean_text() + chunk_text()로 전처리/청킹
+ - Chroma.add_texts()로 벡터스토어에 추가
+ - embedded_markers.txt에 기록
+
+ Args:
+ None
+ Returns:
+ None
+ """
+ to_embed = self.get_unembedded_notes()
+
+ if not to_embed:
+ return
+
+ for note_rel in to_embed:
+ note_path = self.vault_path / note_rel
+ raw_text = note_path.read_text(encoding="utf-8")
+ cleaned = self.clean_text(raw_text)
+
+ # 빈 텍스트는 임베딩 건너뛰기 (Windows 호환성)
+ if not cleaned or not cleaned.strip():
+ # 빈 파일도 마커에 기록하여 다음에 다시 시도하지 않음
+ self.save_embedded_notes([note_rel])
+ continue
+
+ chunks = self.chunk_text(cleaned)
+
+ if len(chunks) > 1:
+ for i, chunk in enumerate(chunks, start=1):
+ # 빈 청크는 건너뛰기
+ if not chunk or not chunk.strip():
+ continue
+ self.vector_store.add_texts(
+ ids=[str(uuid4())],
+ metadatas=[
+ {
+ "title": f"{Path(note_rel).stem}_{i}",
+ "path": note_rel,
+ }
+ ],
+ texts=[chunk],
+ )
+ else:
+ self.vector_store.add_texts(
+ ids=[str(uuid4())],
+ metadatas=[
+ {
+ "title": Path(note_rel).stem,
+ "path": note_rel,
+ }
+ ],
+ texts=[cleaned],
+ )
+
+ self.save_embedded_notes([note_rel])
+
+ # ──────────────────────────────────────────────────────────
+ # 연관 노트 찾기 & 링크 삽입
+ # ──────────────────────────────────────────────────────────
+ def find_related_notes(self, MY_VAULT_PATH: str, k: int = 3) -> list[str]:
+ """
+ 주어진 노트와 의미적으로 유사한 md 파일 경로를 찾습니다.
+ Args:
+ MY_VAULT_PATH (str):
+ vault 기준 상대 경로, 사용자가 input으로 넣을 경로
+ 예) "upthink/data/HCI 2025 학회 강의세션들.md"
+ k (int, optional):
+ 최대 몇 개의 연관 노트를 반환할지. 기본값 3.
+ Returns:
+ list[str]:
+ 연관 노트들의 vault 기준 상대 경로 리스트.
+ (자기 자신은 포함하지 않으며, 중복 제거됨)
+ * frontend에 해당 노트들이 추천되었다는 문구가 간단하게 보여졌으면 좋겠습니다.
+ """
+
+ norm_query = Path(MY_VAULT_PATH).as_posix()
+ query_note_path = self.vault_path / norm_query
+
+ raw_text = query_note_path.read_text(encoding="utf-8")
+ cleaned = self.clean_text(raw_text)
+ query_chunks = self.chunk_text(cleaned)
+
+ # 첫 번째 청크 기준으로 유사도 검색
+ hits = self.vector_store.similarity_search(query_chunks[0], k=k + 4)
+ related: list[str] = []
+ for d in hits:
+ raw_path = d.metadata.get("path", "")
+ if not raw_path:
+ continue
+ norm_path = Path(raw_path).as_posix()
+
+ # 자기 자신은 제외
+ if norm_path == norm_query:
+ continue
+
+ # 중복 제외
+ if norm_path in related:
+ continue
+
+ related.append(norm_path)
+
+ if len(related) >= k:
+ break
+ return related
+
+ def append_related_links(self, MY_VAULT_PATH: str, k: int = 3):
+ """
+ 주어진 노트 파일의 끝에 "Related Notes" 섹션을 추가하고
+ [[연관노트]] 링크를 최대 k개까지 삽입합니다.
+ Args:
+ MY_VAULT_PATH (str):
+ vault 기준 상대 경로.
+ 예) r"upthink\\data\\HCI 2025 학회 강의세션들.md"
+ k (int, optional):
+ 삽입할 링크 개수 (최대). 기본값 3.
+ Returns:
+ None
+ """
+ print(MY_VAULT_PATH)
+ related = self.find_related_notes(MY_VAULT_PATH, k=k)
+ if not related:
+ return
+
+ norm_query = Path(MY_VAULT_PATH).as_posix()
+ target_path = self.vault_path / norm_query
+
+ with target_path.open("a", encoding="utf-8") as f:
+ list_ = []
+ f.write("\n\n## 🔗 Related Notes\n")
+ for path_rel in related[:k]:
+ # Obsidian 링크에서는 확장자(.md)를 떼기 위해 [:-3]
+ f.write(f"[[{path_rel[:-3]}]]\n")
+ list_.append(path_rel[:-3])
+ return list_
+
+
+# ──────────────────────────────────────────────────────────
+# 단독 실행용 예시
+# ──────────────────────────────────────────────────────────
+if __name__ == "__main__":
+ # Vault 경로를 지정해야 합니다
+ MY_VAULT_PATH = "YOUR_VAULT_PATH_HERE" # 예: "/Users/username/Documents/MyVault"
+
+ engine = Related_Note(vault_path=MY_VAULT_PATH)
+
+ # 1) 아직 임베딩 안 된 노트들 임베딩
+ engine.index_unembedded_notes()
+
+ # 2) 특정 노트에 대해 연관 노트 3개 링크 삽입
+ engine.append_related_links(r"upthink\data\HCI 2025 학회 강의세션들.md", k=3)
diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/__init__.py b/usecase/solar-knowledge-management/backend/tag_suggest/__init__.py
new file mode 100644
index 0000000..b2a4df7
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/tag_suggest/__init__.py
@@ -0,0 +1,15 @@
+from .tag_extractor import TagExtractor
+from .tag_guidelines import GuidelineGenerator, ChecklistType
+from .tag_generator import TagGenerator
+from .tag_comparator import TagComparator, TagMatch
+from .markdown_processor import add_yaml_frontmatter
+
+__all__ = [
+ "TagExtractor",
+ "GuidelineGenerator",
+ "ChecklistType",
+ "TagGenerator",
+ "TagComparator",
+ "TagMatch",
+ "add_yaml_frontmatter",
+]
diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/markdown_processor.py b/usecase/solar-knowledge-management/backend/tag_suggest/markdown_processor.py
new file mode 100644
index 0000000..da03362
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/tag_suggest/markdown_processor.py
@@ -0,0 +1,43 @@
+"""
+마크다운 파일 처리 유틸리티
+
+YAML frontmatter 추가 등 마크다운 파일 관련 기능 제공
+"""
+
+from datetime import datetime
+from typing import List
+
+
+def add_yaml_frontmatter(md_content: str, tags: List[str]) -> str:
+ """
+ 마크다운 파일 최상단에 YAML frontmatter를 추가
+
+ Args:
+ md_content: 원본 마크다운 내용
+ tags: 추가할 태그 리스트
+
+ Returns:
+ YAML frontmatter가 추가된 마크다운 내용
+
+ Examples:
+ >>> content = "# Hello World"
+ >>> result = add_yaml_frontmatter(content, ["python", "tutorial"])
+ >>> "tags:" in result
+ True
+ """
+ today = datetime.now().strftime("%Y-%m-%d")
+
+ # 태그 리스트를 YAML 형식으로 변환
+ tags_yaml = "\n".join([f" - {tag}" for tag in tags])
+
+ # YAML frontmatter 생성
+ frontmatter = f"""---
+created: {today}
+modified: {today}
+tags:
+{tags_yaml}
+---
+
+"""
+
+ return frontmatter + md_content
diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/tag_comparator.py b/usecase/solar-knowledge-management/backend/tag_suggest/tag_comparator.py
new file mode 100644
index 0000000..7d77534
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/tag_suggest/tag_comparator.py
@@ -0,0 +1,313 @@
+"""
+신규 생성된 태그와 기존 태그를 비교하여 매칭 및 추천
+
+Qwen Embedding 모델을 사용하여 의미적 유사도 계산
+"""
+
+import numpy as np
+from typing import List, Optional
+from dataclasses import dataclass
+
+from sentence_transformers import SentenceTransformer
+
+from .tag_extractor import TagExtractor
+
+
+@dataclass
+class TagMatch:
+ """태그 매칭 결과"""
+
+ new_tag: str # 신규 생성된 태그
+ matched_tag: Optional[str] # 매칭된 기존 태그 (없으면 None)
+ similarity: float # 유사도 점수 (0.0 ~ 1.0)
+ is_new: bool # 완전히 새로운 태그인지 여부
+
+
+class TagComparator:
+ """신규 태그와 기존 태그를 비교하여 매칭 및 추천"""
+
+ def __init__(
+ self,
+ similarity_threshold: float = 0.85,
+ model_name: str = "Qwen/Qwen3-Embedding-0.6B",
+ ):
+ """
+ TagComparator 초기화
+
+ Args:
+ similarity_threshold: 유사도 임계값 (이상이면 같은 태그로 간주)
+ model_name: 사용할 임베딩 모델명
+ """
+ print(f"[INFO] Embedding 모델 로딩 중: {model_name}")
+ self.model = SentenceTransformer(model_name)
+ self.similarity_threshold = similarity_threshold
+ self.tag_extractor = TagExtractor()
+
+ def _get_embeddings(self, texts: List[str]) -> np.ndarray:
+ """
+ 텍스트 리스트를 embedding 벡터로 변환
+
+ Args:
+ texts: 변환할 텍스트 리스트
+
+ Returns:
+ embedding 벡터 배열 (shape: [len(texts), embedding_dim])
+
+ Raises:
+ ValueError: 빈 텍스트가 포함된 경우
+ """
+ if not texts:
+ return np.array([])
+
+ # 빈 문자열 필터링 및 검증
+ valid_texts = [text.strip() for text in texts if text and text.strip()]
+ if len(valid_texts) != len(texts):
+ print(
+ f"[WARNING] {len(texts) - len(valid_texts)}개의 빈 태그가 제거되었습니다"
+ )
+
+ if not valid_texts:
+ raise ValueError("유효한 텍스트가 없습니다")
+
+ # SentenceTransformer로 embedding 생성 (기존 태그/신규 태그 모두 document로 처리)
+ embeddings = self.model.encode(valid_texts)
+
+ return embeddings
+
+ def compare_tags(
+ self, new_tags: List[str], existing_tags: List[str]
+ ) -> List[TagMatch]:
+ """
+ 신규 태그와 기존 태그를 비교하여 매칭
+
+ Args:
+ new_tags: 신규 생성된 태그 리스트
+ existing_tags: 기존 태그 리스트
+
+ Returns:
+ 각 신규 태그에 대한 매칭 결과 리스트
+
+ Raises:
+ ValueError: 유효하지 않은 태그가 포함된 경우
+ Exception: Embedding 생성 실패 시
+ """
+ if not new_tags:
+ return []
+
+ # 빈 문자열 필터링
+ valid_new_tags = [tag.strip() for tag in new_tags if tag and tag.strip()]
+ if len(valid_new_tags) != len(new_tags):
+ print(
+ f"[WARNING] {len(new_tags) - len(valid_new_tags)}개의 빈 신규 태그가 제거되었습니다"
+ )
+
+ if not valid_new_tags:
+ raise ValueError("유효한 신규 태그가 없습니다")
+
+ if not existing_tags:
+ # 기존 태그가 없으면 모두 새로운 태그
+ return [
+ TagMatch(new_tag=tag, matched_tag=None, similarity=0.0, is_new=True)
+ for tag in valid_new_tags
+ ]
+
+ valid_existing_tags = [
+ tag.strip() for tag in existing_tags if tag and tag.strip()
+ ]
+ if not valid_existing_tags:
+ print("[WARNING] 유효한 기존 태그가 없어 모든 태그를 신규로 처리합니다")
+ return [
+ TagMatch(new_tag=tag, matched_tag=None, similarity=0.0, is_new=True)
+ for tag in valid_new_tags
+ ]
+
+ try:
+ # Embedding 생성 (신규 태그와 기존 태그 모두 같은 공간에서 처리)
+ new_tag_embeddings = self._get_embeddings(valid_new_tags)
+ existing_tag_embeddings = self._get_embeddings(valid_existing_tags)
+
+ # 모든 신규 태그와 기존 태그 간의 유사도를 한번에 계산
+ all_similarities = self.model.similarity(
+ new_tag_embeddings, existing_tag_embeddings
+ )
+
+ results = []
+
+ # 각 신규 태그에 대해 가장 유사한 기존 태그 찾기
+ for i, new_tag in enumerate(valid_new_tags):
+ # i번째 신규 태그의 모든 유사도
+ similarities = all_similarities[i]
+
+ # 가장 유사한 태그 찾기
+ max_similarity_idx = np.argmax(similarities)
+ max_similarity = float(similarities[max_similarity_idx])
+
+ # 임계값 이상이면 매칭, 아니면 새로운 태그
+ if max_similarity >= self.similarity_threshold:
+ matched_tag = valid_existing_tags[max_similarity_idx]
+ is_new = False
+ else:
+ matched_tag = None
+ is_new = True
+
+ results.append(
+ TagMatch(
+ new_tag=new_tag,
+ matched_tag=matched_tag,
+ similarity=max_similarity,
+ is_new=is_new,
+ )
+ )
+
+ return results
+
+ except Exception as e:
+ raise Exception(f"태그 비교 중 오류 발생: {str(e)}") from e
+
+ def get_recommended_tags(
+ self, new_tags: List[str], vault_path: str
+ ) -> List[TagMatch]:
+ """
+ Vault에서 기존 태그를 추출하고 신규 태그와 비교
+
+ Args:
+ new_tags: 신규 생성된 태그 리스트
+ vault_path: Vault 디렉토리 경로
+
+ Returns:
+ 각 신규 태그에 대한 매칭 결과 리스트
+ """
+ # Vault에서 기존 태그 추출
+ existing_tags = list(self.tag_extractor.get_unique_tags(vault_path))
+
+ # 비교 및 매칭
+ return self.compare_tags(new_tags, existing_tags)
+
+ @staticmethod
+ def get_final_tags(matches: List[TagMatch]) -> List[str]:
+ """
+ 매칭 결과를 기반으로 최종 추천 태그 리스트 생성
+
+ Args:
+ matches: 태그 매칭 결과 리스트
+
+ Returns:
+ 최종 추천 태그 리스트 (기존 태그로 대체되거나 새로운 태그, 중복 제거됨)
+ """
+ final_tags = []
+ seen_tags = set()
+
+ for match in matches:
+ if match.is_new:
+ # 새로운 태그
+ tag = match.new_tag
+ else:
+ # 기존 태그로 대체
+ tag = match.matched_tag
+
+ # 중복 체크 후 추가
+ if tag not in seen_tags:
+ final_tags.append(tag)
+ seen_tags.add(tag)
+
+ return final_tags
+
+
+if __name__ == "__main__":
+ import pathlib
+ import traceback
+ from .tag_guidelines import GuidelineGenerator, ChecklistType
+ from .tag_generator import TagGenerator
+
+ # 설정
+ VAULT_PATH = "YOUR_PATH_HERE"
+ TEST_FILE = "YOUR_FILE_HERE"
+
+ print("=" * 60)
+ print("전체 태그 추천 파이프라인 테스트")
+ print("=" * 60)
+
+ try:
+ # 1. Vault에서 기존 태그 추출
+ print("\n[1단계] Vault에서 기존 태그 추출 중...")
+ comparator = TagComparator()
+
+ if not pathlib.Path(VAULT_PATH).exists():
+ print(f"[WARNING] Vault 경로를 찾을 수 없습니다: {VAULT_PATH}")
+ print("[INFO] 기존 태그 없이 테스트를 진행합니다")
+ existing_tags = []
+ else:
+ existing_tags = list(comparator.tag_extractor.get_unique_tags(VAULT_PATH))
+ print(f"✓ 기존 태그 {len(existing_tags)}개 발견")
+ print(f" 예시: {sorted(existing_tags)[:10]}")
+ if len(existing_tags) > 10:
+ print(f" ... 외 {len(existing_tags) - 10}개")
+
+ # 2. 신규 태그 생성
+ print(f"\n[2단계] '{TEST_FILE}'에서 신규 태그 생성 중...")
+
+ # 체크리스트 설정
+ checklist: ChecklistType = {
+ "language": "en",
+ "case_style": "lowercase",
+ "separator": "hyphen",
+ "tag_count_range": {"min": 2, "max": 3},
+ }
+
+ # 가이드라인 생성기 초기화
+ guideline_gen = GuidelineGenerator(checklist)
+
+ # 태그 생성기 초기화
+ tag_gen = TagGenerator()
+
+ # 마크다운 파일 읽기
+ data_path = pathlib.Path(__file__).parent.parent.parent / "data" / TEST_FILE
+
+ if not data_path.exists():
+ print(f"[ERROR] 파일을 찾을 수 없습니다: {data_path}")
+ exit(1)
+
+ with open(data_path, "r", encoding="utf-8") as f:
+ md_content = f.read()
+
+ # 태그 생성
+ new_tags = tag_gen.generate_tags(guideline_gen, md_content, TEST_FILE)
+ print(f"✓ 신규 태그 생성 완료: {new_tags}")
+ print(f" 태그 개수: {len(new_tags)}")
+
+ # 3. 태그 비교 및 매칭
+ print(f"\n[3단계] 신규 태그와 기존 태그 비교 중...")
+ matches = comparator.compare_tags(new_tags, existing_tags)
+
+ print("\n[매칭 결과]")
+ print("-" * 60)
+ for match in matches:
+ status = "✓ 매칭됨" if not match.is_new else "✗ 신규"
+ matched_info = (
+ f"→ {match.matched_tag}" if match.matched_tag else "→ 추가 필요"
+ )
+ print(
+ f"{status} | {match.new_tag:30s} {matched_info:25s} (유사도: {match.similarity:.3f})"
+ )
+
+ # 4. 최종 추천 태그
+ final_tags = TagComparator.get_final_tags(matches)
+ print(f"\n[최종 추천 태그]")
+ print("-" * 60)
+ print(final_tags)
+ print(f" 최종 태그 개수: {len(final_tags)}")
+
+ # 통계
+ new_count = sum(1 for m in matches if m.is_new)
+ matched_count = len(matches) - new_count
+ print(f"\n[통계]")
+ print(f" 신규 태그: {new_count}개")
+ print(f" 매칭된 태그: {matched_count}개")
+
+ print("\n" + "=" * 60)
+ print("테스트 완료!")
+
+ except Exception as e:
+ print(f"\n[ERROR] 테스트 실패: {e}")
+ traceback.print_exc()
+ exit(1)
diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/tag_extractor.py b/usecase/solar-knowledge-management/backend/tag_suggest/tag_extractor.py
new file mode 100644
index 0000000..bebbb15
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/tag_suggest/tag_extractor.py
@@ -0,0 +1,185 @@
+"""
+사용자의 Vault 폴더 경로를 입력 받아서, 정규표현식으로 태그를 추출하게 됨
+
+태그 형식은 2가지임
+1. 해시태그
+ #upstage #solar-pro2
+2. YAML (노트 최상단 frontmatter)
+ ---
+ tags:
+ - upstage
+ - solar-pro2
+ ---
+"""
+
+import re
+import yaml
+from pathlib import Path
+from typing import List, Set, Dict
+from collections import Counter
+
+
+class TagExtractor:
+ """Vault 내 노트들에서 태그를 추출하는 클래스"""
+
+ def __init__(self) -> None:
+ # 해시태그 패턴
+ self.HASHTAG_PATTERN = r"(?:^|\s)#([a-zA-Z0-9가-힣_-]+)"
+ # YAML 패턴
+ self.YAML_PATTERN = r"\A---\s*\n(.*?)\n---"
+ # YAML tag key
+ self.YAML_TAG_KEY = "tags"
+
+ def find_hash_tags(self, content: str) -> List[str]:
+ """
+ 해시태그 패턴의 태그 찾기 (e.g. #upstage)
+
+ Args:
+ content: 노트 내용
+
+ Returns:
+ 추출된 태그 리스트
+ """
+ hash_tags = re.findall(self.HASHTAG_PATTERN, content)
+ return hash_tags
+
+ def find_yaml_tags(self, content: str) -> List[str]:
+ """
+ YAML 패턴에서 태그 찾기
+
+ Args:
+ content: 노트 내용
+
+ Returns:
+ 추출된 태그 리스트
+ """
+ yaml_tags = []
+ match = re.search(self.YAML_PATTERN, content, re.DOTALL)
+ if match:
+ try:
+ frontmatter = yaml.safe_load(match.group(1))
+ if isinstance(frontmatter, dict):
+ for key, value in frontmatter.items():
+ if key.lower() in self.YAML_TAG_KEY:
+ if isinstance(value, list):
+ # if tag가 None이거나 빈 문자열("")이 아니면 리스트에 포함
+ yaml_tags.extend([str(tag) for tag in value if tag])
+ elif value:
+ yaml_tags.append(str(value))
+ except yaml.YAMLError:
+ # 빈 리스트 반환
+ pass
+
+ return yaml_tags
+
+ def extract_tags_from_note(self, content: str) -> Set[str]:
+ """
+ 노트에서 모든 태그를 추출
+
+ Args:
+ content: 노트 내용
+
+ Returns:
+ 추출된 태그의 집합
+ """
+ tags = set()
+ tags.update(self.find_hash_tags(content))
+ tags.update(self.find_yaml_tags(content))
+
+ return tags
+
+ def extract_tags_from_vault(self, vault_path: str) -> Dict[str, List[str]]:
+ """
+ Vault 내 모든 노트에서 태그를 추출
+
+ Args:
+ vault_path: Vault 디렉토리 경로
+
+ Returns:
+ 파일 경로는 key, 태그 리스트를 value로 하는 딕셔너리
+ """
+ vault_dir = Path(vault_path)
+
+ if not vault_dir.exists() or not vault_dir.is_dir():
+ raise ValueError(f"유효하지 않은 Vault 경로: {vault_path}")
+
+ tags_by_file = {}
+
+ # 모든 노트 순회
+ for md_file in vault_dir.rglob("*.md"):
+ try:
+ content = md_file.read_text(encoding="utf-8")
+ tags = self.extract_tags_from_note(content)
+
+ if tags:
+ tags_by_file[str(md_file)] = sorted(list(tags))
+ except Exception as e:
+ print(f"파일 읽기 실패 {md_file}: {e}")
+ continue
+
+ return tags_by_file
+
+ def get_unique_tags(self, vault_path: str) -> Set[str]:
+ """
+ 유니크한 태그만 있도록
+
+ Args:
+ vault_path: Vault 디렉토리 경로
+
+ Returns:
+ 유니크한 태그의 집합
+ """
+ tags_by_file = self.extract_tags_from_vault(vault_path)
+ unique_tags = set()
+ for tags in tags_by_file.values():
+ unique_tags.update(tags)
+
+ return unique_tags
+
+ def count_tags(self, vault_path: str) -> Dict[str, int]:
+ """Vault 내 태그 빈도 계산"""
+ tags_by_file = self.extract_tags_from_vault(vault_path)
+ tag_counts = Counter()
+ for tags in tags_by_file.values():
+ tag_counts.update(tags)
+
+ return dict(tag_counts.most_common())
+
+
+if __name__ == "__main__":
+ tag_extractor = TagExtractor()
+
+ MY_VAULT_PATH = "YOUR_PATH_HERE" # 절대 경로로 입력
+
+ print(f"'{MY_VAULT_PATH}' 경로에서 태그 추출 시작")
+ print("-" * 40)
+
+ try:
+ # 태그 빈도수 계산
+ print("\n태그 빈도수 (가장 많이 사용된 상위 20개)")
+ tag_counts = tag_extractor.count_tags(MY_VAULT_PATH)
+
+ if not tag_counts:
+ print(" -> 태그를 찾을 수 없습니다.")
+ else:
+ # 상위 20개만 출력 (너무 많을 경우 대비)
+ for tag, count in list(tag_counts.items())[:20]:
+ print(f" - {tag}: {count}회")
+ if len(tag_counts) > 20:
+ print(f" ... (외 {len(tag_counts) - 20}개 태그)")
+
+ # 전체 고유 태그 목록
+ print("\n전체 고유 태그 목록 (알파벳 순)")
+ unique_tags = tag_extractor.get_unique_tags(MY_VAULT_PATH)
+
+ if not unique_tags:
+ print(" -> 태그를 찾을 수 없습니다.")
+ else:
+ print(f" -> 총 {len(unique_tags)}개의 고유 태그 발견")
+ print(sorted(list(unique_tags)))
+
+ except ValueError as e:
+ print(f"❌ 경로 오류가 발생했습니다: {e}")
+ print("MY_VAULT_PATH 변수에 올바른 Vault 폴더 경로를 입력했는지 확인하세요.")
+ except Exception as e:
+ print(f"❌ 예상치 못한 오류가 발생했습니다: {e}")
diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/tag_generator.py b/usecase/solar-knowledge-management/backend/tag_suggest/tag_generator.py
new file mode 100644
index 0000000..a029782
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/tag_suggest/tag_generator.py
@@ -0,0 +1,222 @@
+"""
+가이드라인과 markdown 파일을 받아, 신규 태그를 생성
+"""
+
+import os
+import json
+
+from openai import OpenAI
+from dotenv import load_dotenv
+
+from .tag_guidelines import GuidelineGenerator
+
+load_dotenv()
+
+
+class TagGenerator:
+ """LLM을 사용하여 마크다운 내용을 기반으로 태그 생성"""
+
+ def __init__(
+ self,
+ model: str = "solar-pro2",
+ temperature: float = 0.3,
+ max_tokens: int = 1000,
+ reasoning_effort: str = "high",
+ timeout: int = 300,
+ ):
+ """
+ TagGenerator 초기화
+
+ Args:
+ model: 사용할 LLM 모델 (기본값: "solar-pro2")
+ temperature: 생성 다양성 (0.0-1.0, 기본값: 0.3)
+ max_tokens: 최대 토큰 수 (기본값: 1000)
+ reasoning_effort: reasoning 강도 (기본값: "high")
+ timeout: API 타임아웃 (초 단위, 기본값: 300 = 5분)
+ """
+ api_key = os.getenv("UPSTAGE_API_KEY")
+ if not api_key:
+ raise ValueError("UPSTAGE_API_KEY 환경 변수가 설정되지 않았습니다")
+
+ self.client = OpenAI(
+ api_key=api_key,
+ base_url="https://api.upstage.ai/v1",
+ timeout=timeout,
+ )
+ self.model = model
+ self.temperature = temperature
+ self.max_tokens = max_tokens
+ self.reasoning_effort = reasoning_effort
+
+ def _parse_llm_response(self, response: str) -> list[str]:
+ """
+ LLM 응답을 파싱하여 태그 리스트 추출
+
+ Args:
+ response: LLM의 원본 응답
+
+ Returns:
+ 파싱된 태그 리스트
+
+ Raises:
+ ValueError: JSON 파싱 실패 시
+ """
+ if not response:
+ raise ValueError("LLM 응답이 비어있습니다")
+
+ # 다양한 형식의 코드 블록 제거
+ cleaned_response = response.strip()
+
+ if cleaned_response.startswith("```"):
+ # 첫 번째 줄 제거 (```json, ```JSON, ``` 등)
+ lines = cleaned_response.split("\n", 1)
+ if len(lines) > 1:
+ cleaned_response = lines[1]
+ else:
+ cleaned_response = cleaned_response[3:]
+
+ # 마지막 ``` 제거
+ if cleaned_response.endswith("```"):
+ cleaned_response = cleaned_response[:-3]
+
+ cleaned_response = cleaned_response.strip()
+
+ # JSON 파싱
+ try:
+ parsed = json.loads(cleaned_response)
+ tags = parsed.get("tags", [])
+ if not isinstance(tags, list):
+ raise ValueError("tags가 리스트 형식이 아닙니다")
+ return tags
+ except json.JSONDecodeError as e:
+ raise ValueError(
+ f"LLM 응답을 JSON으로 파싱할 수 없습니다: {cleaned_response[:200]}"
+ ) from e
+
+ def generate_tags(
+ self,
+ guideline_generator: GuidelineGenerator,
+ md_content: str,
+ filename: str,
+ max_retries: int = 5,
+ ) -> list[str]:
+ """
+ 가이드라인과 마크다운 내용을 기반으로 태그 생성
+
+ Args:
+ guideline_generator: 태그 생성 가이드라인 생성기
+ md_content: 마크다운 파일 내용
+ filename: 파일명 (LLM 컨텍스트 제공용)
+ max_retries: 최대 재시도 횟수 (기본값: 5)
+
+ Returns:
+ 생성된 태그 리스트
+
+ Raises:
+ ValueError: LLM 응답이 올바른 JSON 형식이 아닌 경우
+ Exception: LLM API 호출 실패 시
+ """
+ # 가이드라인 생성
+ system_prompt = guideline_generator.generate_guideline()
+
+ # 사용자 메시지 구성
+ user_message = f"markdown 파일: {filename}\n\n{md_content}"
+
+ last_error = None
+
+ for attempt in range(max_retries):
+ try:
+ # LLM API 호출
+ print(f"[DEBUG] 태그 생성 시작 (시도 {attempt + 1}/{max_retries})")
+ print(f"[DEBUG] 모델: {self.model}, reasoning_effort: {self.reasoning_effort}")
+ print(f"[DEBUG] API 호출 중... (최대 {self.client.timeout}초 대기)")
+
+ stream = self.client.chat.completions.create(
+ model=self.model,
+ messages=[
+ {
+ "role": "system",
+ "content": system_prompt,
+ },
+ {
+ "role": "user",
+ "content": user_message,
+ },
+ ],
+ max_tokens=self.max_tokens,
+ temperature=self.temperature,
+ reasoning_effort=self.reasoning_effort,
+ stream=False,
+ )
+
+ print(f"[DEBUG] API 응답 수신 완료")
+
+ # 응답 추출
+ if not stream.choices:
+ raise ValueError("LLM 응답에 choices가 없습니다")
+
+ response = stream.choices[0].message.content
+ print(f"[DEBUG] 응답 파싱 시작")
+
+ # 응답 파싱
+ tags = self._parse_llm_response(response)
+ print(f"[DEBUG] 태그 생성 완료: {len(tags)}개")
+ return tags
+
+ except ValueError as e:
+ last_error = e
+ if attempt < max_retries - 1:
+ print(
+ f"[WARNING] 태그 생성 실패 (시도 {attempt + 1}/{max_retries}): {e}"
+ )
+ # temperature를 약간 조정하여 재시도
+ self.temperature = min(self.temperature + 0.1, 1.0)
+ continue
+ except Exception as e:
+ raise Exception(f"태그 생성 중 오류 발생: {str(e)}") from e
+
+ # 모든 재시도 실패
+ raise Exception(
+ f"태그 생성 실패 ({max_retries}회 시도): {str(last_error)}"
+ ) from last_error
+
+
+if __name__ == "__main__":
+ import pathlib
+ from backend.tag_suggest import ChecklistType, GuidelineGenerator
+
+ # 테스트용 체크리스트
+ checklist: ChecklistType = {
+ "language": "en",
+ "case_style": "lowercase",
+ "separator": "hyphen",
+ "tag_count_range": {"min": 3, "max": 5},
+ }
+
+ # 가이드라인 생성기 초기화
+ guideline_gen = GuidelineGenerator(checklist)
+ tag_gen = TagGenerator()
+
+ # 테스트용 마크다운 파일
+ FILENAME = "YOUR_FILE_HERE"
+ DATA_PATH = pathlib.Path(__file__).parent.parent.parent / "data" / FILENAME
+
+ if not DATA_PATH.exists():
+ print(f"[ERROR] 파일을 찾을 수 없습니다: {DATA_PATH}")
+ exit(1)
+
+ print(f"[INFO] 파일 경로: {DATA_PATH}")
+
+ with open(DATA_PATH, "r", encoding="utf-8") as f:
+ md_content = f.read()
+
+ try:
+ print(f"[INFO] 태그 생성 시작")
+ tags = tag_gen.generate_tags(guideline_gen, md_content, FILENAME)
+ print(f"\n[SUCCESS] 생성된 태그: {tags}")
+ print(f"[INFO] 태그 개수: {len(tags)}")
+ except Exception as e:
+ print(f"\n[ERROR] 에러 발생: {e}")
+ import traceback
+
+ traceback.print_exc()
diff --git a/usecase/solar-knowledge-management/backend/tag_suggest/tag_guidelines.py b/usecase/solar-knowledge-management/backend/tag_suggest/tag_guidelines.py
new file mode 100644
index 0000000..48c7ee1
--- /dev/null
+++ b/usecase/solar-knowledge-management/backend/tag_suggest/tag_guidelines.py
@@ -0,0 +1,190 @@
+"""
+태그 작성을 위한 가이드라인 생성
+
+사용자의 체크리스트를 받아서 LLM에게 태그 작성 가이드라인을 전달
+
+---
+✅ 체크리스트
+주로 사용하는 언어
+영어 사용 시 대소문자 규칙
+단어의 구분자
+태그 개수
+"""
+
+from typing import Dict, Any, TypedDict
+
+
+class ChecklistType(TypedDict, total=False):
+ """태그 작성 패턴 체크리스트 타입"""
+
+ language: str # ko, en
+ case_style: str # lowercase, uppercase (영어 사용 시)
+ separator: str # hyphen, underscore
+ tag_count_range: Dict[str, int] # min, max
+
+
+class GuidelineGenerator:
+ """태그 작성 가이드라인 생성 클래스"""
+
+ def __init__(self, checklist: ChecklistType) -> None:
+ self.checklist = checklist
+ self._validate_checklist()
+
+ def _validate_checklist(self) -> None:
+ """체크리스트 유효성 검사"""
+ required_fields = [
+ "language",
+ "separator",
+ "tag_count_range",
+ ]
+
+ for field in required_fields:
+ if field not in self.checklist:
+ raise ValueError(f"필수 항목이 누락되었습니다: {field}")
+
+ # 영어 사용 시 case_style 필수
+ if self.checklist["language"] in ["en"]:
+ if "case_style" not in self.checklist:
+ raise ValueError("영어 사용 시 대소문자 설정은 필수입니다")
+
+ if self.checklist["case_style"] not in ["lowercase", "uppercase"]:
+ raise ValueError("영어 사용 시 대소문자 설정은 필수입니다")
+
+ # tag_count_range 검증
+ tag_range = self.checklist.get("tag_count_range", {})
+
+ min_val = tag_range["min"]
+ max_val = tag_range["max"]
+
+ if "min" not in tag_range or "max" not in tag_range:
+ raise ValueError("태그의 최소, 최대 개수 설정이 필요합니다")
+
+ if min_val > max_val:
+ raise ValueError(f"'{min_val}'은 '{max_val}'보다 작거나 같아야 합니다")
+
+ if min_val < 2 or max_val > 10:
+ raise ValueError("태그 개수는 최소 2개, 최대 10개로 제한됩니다")
+
+ def generate_guideline(self) -> str:
+ """
+ 체크리스트를 바탕으로 태그 작성 가이드라인 생성
+
+ Returns:
+ LLM 프롬프트에 사용할 가이드라인 문자열
+ """
+ guideline_parts = [
+ "# 태그 작성 가이드라인\n",
+ "다음 규칙을 **반드시** 준수하여 태그를 생성해 주세요.\n",
+ ]
+
+ # 주로 사용하는 언어
+ guideline_parts.append(self._generate_language_rule())
+
+ # 영어 사용 시 대소문자 규칙
+ if self.checklist["language"] in ["en"]:
+ guideline_parts.append(self._generate_case_rule())
+
+ # 단어의 구분이 필요할 경우
+ guideline_parts.append(self._generate_separator_rule())
+
+ # 선호하는 태그 개수
+ guideline_parts.append(self._generate_count_rule())
+
+ # Output 형식
+ guideline_parts.append(self._generate_output_format())
+
+ return "\n".join(guideline_parts)
+
+ def _generate_language_rule(self) -> str:
+ """언어 규칙 생성"""
+ language = self.checklist["language"]
+
+ language_map = {
+ "ko": "**한국어**만 사용하세요.",
+ "en": "**영어**만 사용하세요.",
+ }
+
+ rule = language_map[language]
+ return f"## 주로 사용하는 언어\n{rule}\n"
+
+ def _generate_case_rule(self) -> str:
+ """대소문자 규칙 생성 (영어 사용 시)"""
+ case_style = self.checklist.get("case_style", "lowercase")
+
+ case_map = {
+ "lowercase": f"**소문자**만 사용하세요.",
+ "uppercase": f"**대문자**만 사용하세요.",
+ }
+
+ rule = case_map[case_style]
+ return f"## 대소문자 규칙\n{rule}\n"
+
+ def _generate_separator_rule(self) -> str:
+ """단어 구분자 규칙 생성"""
+ separator = self.checklist["separator"]
+
+ separator_map = {
+ "hyphen": "단어의 사이를 **하이픈(-)**으로 구분하세요.",
+ "underscore": "단어의 사이를 **언더스코어(_)**로 구분하세요.",
+ }
+
+ rule = separator_map[separator]
+ return f"## 단어에서 구분자가 필요한 경우\n{rule}\n"
+
+ def _generate_count_rule(self) -> str:
+ """태그 개수 규칙 생성"""
+ tag_range = self.checklist["tag_count_range"]
+ min_count = tag_range["min"]
+ max_count = tag_range["max"]
+
+ return (
+ f"## 태그 개수 (CRITICAL)\n"
+ f"**반드시 {min_count}개 이상 {max_count}개 이하의 태그만 생성하세요.**\n"
+ f"- {min_count}개보다 적으면 안 됩니다.\n"
+ f"- {max_count}개보다 많으면 안 됩니다.\n"
+ f"- 이 규칙을 위반하면 응답이 거부됩니다.\n"
+ )
+
+ def _generate_output_format(self) -> str:
+ """Output 형식 규칙 생성"""
+ return (
+ "# Output Format\n"
+ "Return ONLY the JSON object:\n"
+ "```json\n"
+ '{"tags": ["...", "...", "...", ..., "..."]}\n'
+ "```"
+ )
+
+ def get_summary(self) -> Dict[str, Any]:
+ """
+ 체크리스트 요약 정보 반환
+
+ Returns:
+ 체크리스트의 주요 설정을 요약한 딕셔너리
+ """
+ summary = {
+ "언어": self.checklist["language"],
+ "대소문자": self.checklist.get("case_style", "N/A"),
+ "구분자": self.checklist["separator"],
+ "태그 개수": f"{self.checklist['tag_count_range']['min']}-{self.checklist['tag_count_range']['max']}개",
+ }
+
+ return summary
+
+
+if __name__ == "__main__":
+ test_checklist: ChecklistType = {
+ "language": "en",
+ "case_style": "lowercase",
+ "separator": "hyphen",
+ "tag_count_range": {"min": 3, "max": 5},
+ }
+
+ guidelines_generator = GuidelineGenerator(test_checklist)
+ summary = guidelines_generator.get_summary()
+ print("\n체크리스트 요약:")
+ for key, value in summary.items():
+ print(f" - {key}: {value}")
+
+ print("\n생성된 가이드라인:")
+ print(guidelines_generator.generate_guideline())
diff --git a/usecase/solar-knowledge-management/frontend/app.py b/usecase/solar-knowledge-management/frontend/app.py
new file mode 100644
index 0000000..a32c09b
--- /dev/null
+++ b/usecase/solar-knowledge-management/frontend/app.py
@@ -0,0 +1,88 @@
+"""
+UpThink 메인 앱
+"""
+
+import os
+import streamlit as st
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+st.set_page_config(page_title="UpThink", page_icon="💭", layout="wide")
+
+# API Key 설정
+UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY")
+
+
+# 공통 사이드바 설정
+def render_common_sidebar():
+ """모든 페이지에서 공통으로 사용하는 사이드바"""
+ with st.sidebar:
+ # Vault 경로 입력
+ st.text_input(
+ "Vault 경로",
+ placeholder="Obsidian Vault의 경로를 입력하세요",
+ help="Obsidian Vault 디렉토리의 절대 경로를 입력하세요",
+ key="vault_path",
+ )
+
+ # 파일 업로드
+ st.file_uploader(
+ "Markdown 파일 업로드",
+ type=["md"],
+ help="처리할 마크다운 파일을 업로드하세요",
+ key="uploaded_file",
+ )
+
+
+# 공통 사이드바 렌더링
+render_common_sidebar()
+
+
+home = st.Page(
+ "home.py",
+ title="Intro",
+ icon=":material/home:",
+ default=True,
+)
+
+image_ocr = st.Page(
+ "image_ocr.py",
+ title="이미지 대체 텍스트 생성",
+ icon=":material/image_search:",
+)
+tag_suggest = st.Page(
+ "tag_suggest.py",
+ title="태그 추천",
+ icon=":material/new_label:",
+)
+related_note = st.Page(
+ "related_note.py",
+ title="연관 노트 추천",
+ icon=":material/note_stack:",
+)
+note_split = st.Page(
+ "note_split.py",
+ title="노트 분할",
+ icon=":material/split_scene:",
+)
+note_freshness = st.Page(
+ "note_freshness.py",
+ title="최신 정보 확인",
+ icon=":material/update:",
+)
+
+pg = st.navigation(
+ {
+ "홈": [home],
+ "노트 정리": [
+ image_ocr,
+ tag_suggest,
+ related_note,
+ note_split,
+ ],
+ "최신성 검증": [note_freshness],
+ }
+)
+pg.run()
diff --git a/usecase/solar-knowledge-management/frontend/home.py b/usecase/solar-knowledge-management/frontend/home.py
new file mode 100644
index 0000000..d3d368b
--- /dev/null
+++ b/usecase/solar-knowledge-management/frontend/home.py
@@ -0,0 +1,78 @@
+"""
+서비스 설명 기재 (최초로 진입하는 페이지)
+"""
+
+import streamlit as st
+
+st.title("💭 UpThink")
+st.caption("""Think + Upstage ✨\\
+지식을 정리하는 사고에만 몰입해 보세요!""")
+
+st.markdown(
+ """### 개요
+개인 지식 관리 환경(Obsidian)에서 노트에 지식을 정리할 때, 가장 중요한 사고의 흐름이 끊긴 적 있으신가요?
+
+▪︎ㅤ노트 내 이미지의 정보를 직접 옮겨 적거나 \\
+▪︎ㅤ태그의 대소문자나 구분자 등 스타일링 규칙을 고민하거나 \\
+▪︎ㅤ작성 중인 내용과 연관된 과거 노트를 찾기 위해 탐색하거나 \\
+▪︎ㅤ내용이 너무 많아진 노트를 어떻게 분할할지 막막하거나
+
+UpThink는 Upstage Solar Pro 2의 강력한 언어 이해 능력을 활용하여 이러한 지식 관리의 병목 구간을 해결합니다. \\
+이미지 분석부터 태그 정리, 연관 지식 탐색, 노트 분할까지! 번거로운 정리 작업은 AI에게 맡기고, 가장 중요한 사고 활동에만 몰입해 보세요.
+"""
+)
+
+st.divider()
+
+st.markdown("### 시연 영상")
+st.markdown("👇 사용 방법은 시연 영상을 참고해 주세요!")
+st.video("https://www.youtube.com/watch?v=8bjLew7KTW4", width=900)
+
+st.divider()
+
+st.markdown("### 주요 서비스 기능")
+st.markdown(
+ """##### 1️⃣ 이미지 대체 텍스트 생성
+노트 내 이미지를 탐색하여 Upstage Document Parse로 텍스트를 추출한 후, Solar Pro 2를 사용하여 이미지를 설명하는 대체 텍스트를 생성합니다. \\
+생성된 대체 텍스트는 `(대체 텍스트 by Upstage)` 코드 블록으로 이미지 링크 아래에 추가되어, 수정된 Markdown 파일을 다운로드할 수 있습니다."""
+)
+st.image(
+ "https://github-production-user-asset-6210df.s3.amazonaws.com/171089104/527155856-9d7a9c48-0e53-45f3-88d9-1cb0c6ea3981.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20251216%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251216T163232Z&X-Amz-Expires=300&X-Amz-Signature=a295d1d084ff3ae49e59d83cacb69588851300012ae4a143711a8e4116d72fb1&X-Amz-SignedHeaders=host",
+ width=800,
+)
+st.markdown(
+ """##### 2️⃣ 태그 추천
+Obsidian Vault 경로에 있는 모든 Markdown 파일에서 2가지 태그 패턴을 추출합니다. \\
+사용자가 업로드한 파일 내용과 직접 설정한 가이드라인(언어, 포맷 등)을 기반으로 태그를 생성하고, 기존 태그와의 유사도를 비교해 최종 태그를 선별합니다. \\
+최종 선정된 태그 목록은 YAML Frontmatter 형식으로 노트 최상단에 자동으로 추가되어, 사용자가 수정된 Markdown 파일을 다운로드할 수 있습니다."""
+)
+st.image(
+ "https://github-production-user-asset-6210df.s3.amazonaws.com/171089104/527155896-4b950ff7-6a1b-4df9-ac76-afc5f1defac9.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20251216%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251216T163318Z&X-Amz-Expires=300&X-Amz-Signature=24911f26b551015c43a0503775a4281b4d3b5468912e2ec1076b209a82bf900e&X-Amz-SignedHeaders=host",
+ width=800,
+)
+st.markdown(
+ """##### 3️⃣ 연관 노트 추천
+Vault 내 노트를 Upstage Embedding Model로 벡터화하여 Chroma DB에 저장합니다. \\
+업로드한 노트와 유사도가 높은 Top 3 노트를 검색하여 추천합니다. \\
+추천된 노트는 `## Related Notes` 섹션에 백링크 형식으로 자동 삽입됩니다."""
+)
+st.image(
+ "https://github-production-user-asset-6210df.s3.amazonaws.com/171089104/527155923-1ee795f1-9bcc-4916-9bbc-190dff0ee82e.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20251216%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251216T163334Z&X-Amz-Expires=300&X-Amz-Signature=5ac8983cf767cf81996427e2e5046d481fa04d2bde1d5ec5ba3a4fca7baeae02&X-Amz-SignedHeaders=host",
+ width=800,
+)
+st.markdown(
+ """##### 4️⃣ 노트 분할
+Solar Pro 2로 노트에서 주제(Topic)를 자동 추출하고, 사용자가 편집/삭제/추가할 수 있습니다. \\
+각 주제별로 원자 노트를 생성하여 지정된 폴더에 저장합니다. \\
+원본 노트에는 백링크와 `## Generated Atomic Notes` 섹션이 자동으로 추가됩니다."""
+)
+st.image(
+ "https://github-production-user-asset-6210df.s3.amazonaws.com/171089104/527155946-f834e4a6-7227-4dcd-81e3-5087cf5f218c.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20251216%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251216T163350Z&X-Amz-Expires=300&X-Amz-Signature=34e0ed69aa99677f81b7cef22958ef47cd54d01c434ea000cf7e2d9aef2c626a&X-Amz-SignedHeaders=host",
+ width=800,
+)
+
+st.divider()
+
+st.markdown("""**Acknowledgements** \\
+이 프로젝트는 **Upstage AI Ambassador** 활동의 일환으로 진행되었습니다. \\
+프로젝트를 진행할 수 있도록 Credit을 지원해 주신 **[Upstage](https://www.upstage.ai/)** 에 감사드립니다.""")
\ No newline at end of file
diff --git a/usecase/solar-knowledge-management/frontend/image_ocr.py b/usecase/solar-knowledge-management/frontend/image_ocr.py
new file mode 100644
index 0000000..21a2039
--- /dev/null
+++ b/usecase/solar-knowledge-management/frontend/image_ocr.py
@@ -0,0 +1,179 @@
+"""
+노트 내 이미지에서 텍스트 추출 -> 대체 텍스트 생성
+"""
+
+import sys
+from pathlib import Path
+
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root))
+
+import os
+import streamlit as st
+from typing import Optional
+
+from backend.image_ocr import MarkdownImageProcessor
+
+
+def init_session_state():
+ """세션 상태 초기화 (이미지 OCR 전용)"""
+ # vault_path와 uploaded_file은 공통 요소여서, frontend/app.py 에서 관리
+ if "image_ocr_step" not in st.session_state:
+ st.session_state.image_ocr_step = 1
+
+
+def main():
+ """메인 함수"""
+ init_session_state()
+
+ # 메인 헤더
+ st.title("🖼️ 이미지 대체 텍스트 생성")
+ st.caption("노트 내 이미지가 어떤 정보를 가지고 있는지 쉽게 제공받을 수 있습니다!")
+ st.text("")
+
+ # API 키 확인
+ UPSTAGE_API_KEY: Optional[str] = os.getenv("UPSTAGE_API_KEY")
+ if not UPSTAGE_API_KEY:
+ st.error(
+ "⚠️ **UPSTAGE_API_KEY** 환경 변수가 설정되지 않았습니다. "
+ "AI 기능을 사용하려면 터미널에 `export UPSTAGE_API_KEY='YOUR_KEY'` 명령을 실행하고 앱을 재시작하세요."
+ )
+ return
+
+ # Step 1: Vault 경로 및 파일 확인
+ if st.session_state.image_ocr_step == 1:
+ vault_path_str = st.session_state.get("vault_path", "")
+ uploaded_file = st.session_state.get("uploaded_file")
+
+ if vault_path_str and uploaded_file:
+ vault_root = Path(vault_path_str.strip())
+ if not vault_root.is_dir():
+ st.error(
+ f"오류: 입력된 경로 ({vault_path_str})는 유효한 폴더가 아닙니다."
+ )
+ return
+
+ st.info(
+ f"- Vault 경로:ㅤ{vault_path_str}\n"
+ f"- Markdown 파일:ㅤ{uploaded_file.name}\n\n"
+ f"**💡 변경이 필요한 경우 왼쪽 사이드바에서 수정해 주세요.**"
+ )
+ if st.button("이미지 대체 텍스트 생성 시작", type="primary"):
+ st.session_state.image_ocr_step = 2
+ st.rerun()
+ else:
+ st.warning(
+ "👈ㅤ왼쪽 사이드바에서 ***Vault 경로*** 와 ***Markdown 파일 업로드*** 설정을 완료해 주세요."
+ )
+
+ # Step 2: 이미지 처리 (자동 실행)
+ elif st.session_state.image_ocr_step == 2:
+ vault_path_str = st.session_state.get("vault_path", "")
+ uploaded_file = st.session_state.get("uploaded_file")
+ vault_root = Path(vault_path_str.strip())
+
+ try:
+ # 마크다운 내용 읽기
+ md_content = uploaded_file.getvalue().decode("utf-8")
+
+ # 프로세서 초기화
+ processor = MarkdownImageProcessor()
+
+ # 진행 상황 표시
+ progress_container = st.container()
+ with progress_container:
+ st.divider()
+ st.subheader("🔍 OCR 분석 및 LLM 추론")
+ progress_bar = st.progress(0, text="초기화 중...")
+ status_text = st.empty()
+
+ # 진행 상황 콜백 함수
+ def progress_callback(current: int, total: int, img_src: str):
+ progress = current / total
+ progress_bar.progress(progress)
+ status_text.caption(f"[{current}/{total}] '{img_src}' 처리 중...")
+
+ # 이미지 처리 실행
+ processed_md, processed_images = processor.process_images(
+ md_content, vault_root, progress_callback
+ )
+
+ # 진행 상황 표시 완료
+ progress_bar.empty()
+ status_text.empty()
+
+ # 결과를 세션에 저장
+ st.session_state.processed_md = processed_md
+ st.session_state.processed_images = processed_images
+
+ # Step 3로 이동
+ st.session_state.image_ocr_step = 3
+ st.rerun()
+
+ except Exception as e:
+ st.error(f"❌ 이미지 처리 실패: {e}")
+ with st.expander("상세 오류 정보"):
+ import traceback
+
+ st.code(traceback.format_exc())
+
+ # Step 3: 처리 결과 표시
+ elif st.session_state.image_ocr_step == 3:
+ processed_md = st.session_state.get("processed_md", "")
+ processed_images = st.session_state.get("processed_images", [])
+ uploaded_file = st.session_state.get("uploaded_file")
+
+ # 결과 확인
+ if not processed_images:
+ st.info(
+ "🔍 대체 텍스트 생성이 필요한 이미지가 없거나 이미지가 포함되지 않았습니다."
+ )
+ return
+
+ # 처리된 이미지 목록 표시
+ with st.expander("📊 처리된 이미지 목록", expanded=False):
+ for img_info in processed_images:
+ st.caption(
+ f"'{img_info['src']}' 텍스트 생성 완료: *{img_info['new_alt_text'][:50]}...*"
+ )
+
+ st.success(
+ f"✅ 이미지 처리 완료. {len(processed_images)}개 이미지가 업데이트되었습니다."
+ )
+
+ # 결과 표시
+ st.divider()
+ st.subheader("✅ 처리 결과 확인")
+
+ col_download, _, col_reset = st.columns([1, 2, 1])
+
+ with col_reset:
+ if st.button(
+ "🔄ㅤ새로고침",
+ use_container_width=True,
+ type="secondary",
+ help="처음 단계로 돌아갑니다",
+ ):
+ # 세션 데이터 정리
+ if "processed_md" in st.session_state:
+ del st.session_state.processed_md
+ if "processed_images" in st.session_state:
+ del st.session_state.processed_images
+ st.session_state.image_ocr_step = 1
+ st.rerun()
+
+ with col_download:
+ st.download_button(
+ label="⬇️ㅤ다운로드",
+ data=processed_md,
+ file_name=f"processed_{uploaded_file.name}",
+ type="primary",
+ mime="text/markdown",
+ use_container_width=True,
+ )
+
+ st.code(processed_md, language="markdown")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/usecase/solar-knowledge-management/frontend/note_freshness.py b/usecase/solar-knowledge-management/frontend/note_freshness.py
new file mode 100644
index 0000000..e996cce
--- /dev/null
+++ b/usecase/solar-knowledge-management/frontend/note_freshness.py
@@ -0,0 +1,543 @@
+"""Main Streamlit application for Note Freshness Check."""
+
+import streamlit as st
+import sys
+import tempfile
+import pypandoc
+from pathlib import Path
+from typing import Optional
+
+# Add project root to path
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root))
+
+# Define prompts directory
+PROMPTS_DIR = project_root / "prompts"
+
+from backend.note_freshness.config import Config
+from backend.note_freshness.core.state_manager import StateManager
+from backend.note_freshness.core.file_handler import FileHandler
+from backend.note_freshness.core.path_utils import resolve_path, format_path_for_display
+from backend.note_freshness.llm.client import UpstageClient
+from backend.note_freshness.llm.parsers import ResponseParser
+from backend.note_freshness.llm.prompt_loader import PromptLoader
+from backend.note_freshness.api.wikipedia import WikipediaClient
+from backend.note_freshness.api.tavily import TavilyClient
+from backend.note_freshness.ui.components import (
+ render_file_input_section,
+ render_template_selection_section,
+ render_metadata_review_section,
+ render_search_results_section,
+ render_guide_preview,
+ render_error,
+ render_success,
+ render_info,
+)
+
+
+def ensure_pandoc_installed() -> bool:
+ """Ensure pandoc is installed, download if necessary.
+
+ Returns:
+ bool: True if pandoc is available, False otherwise.
+ """
+ try:
+ # Try to get pandoc path to check if it's installed
+ pypandoc.get_pandoc_path()
+ return True
+ except (OSError, RuntimeError):
+ # Pandoc is not installed, download it
+ try:
+ with st.spinner("Pandoc을 다운로드하는 중..."):
+ pypandoc.download_pandoc()
+ st.success("✅ Pandoc이 성공적으로 설치되었습니다.")
+ return True
+ except Exception as e:
+ st.error(f"⚠️ Pandoc 다운로드에 실패했습니다: {str(e)}")
+ st.info(
+ "수동으로 Pandoc을 설치해주세요: https://pandoc.org/installing.html"
+ )
+ return False
+
+
+def initialize_app():
+ """Initialize the application."""
+ Config.ensure_directories()
+ StateManager.initialize()
+
+
+def validate_api_key() -> bool:
+ """Validate that API key is configured."""
+ if not Config.validate():
+ st.error(
+ "⚠️ Upstage API key not found. Please set UPSTAGE_API_KEY in your .env file."
+ )
+ return False
+ return True
+
+
+def get_default_schema() -> str:
+ """Load default extraction schema from file."""
+ loader = PromptLoader(prompts_dir=PROMPTS_DIR)
+ schema = loader.load_schema("info_extract_schema")
+ if schema:
+ return schema
+ # Fallback default
+ return """{
+ "type": "object",
+ "properties": {
+ "info_keyword": {
+ "type": "string",
+ "description": "The most important keyword derived from the document."
+ },
+ "info_query": {
+ "type": "string",
+ "description": "A Korean search query for retrieving up-to-date information."
+ }
+ },
+ "required": ["info_keyword", "info_query"]
+}"""
+
+
+def handle_note_validation(note_path: str, save_folder: str):
+ """Handle the note validation step."""
+ path = resolve_path(note_path)
+
+ if not path.exists():
+ render_error(f"파일을 찾을 수 없습니다: {note_path}")
+ return
+
+ if not path.suffix == ".md":
+ render_error("마크다운 (.md) 파일을 입력해주세요.")
+ return
+
+ # Read the note and check for existing metadata
+ content, metadata = FileHandler.read_note(path)
+ if content is None:
+ render_error("노트 파일을 읽는 데 실패했습니다.")
+ return
+
+ # Set paths in state
+ StateManager.set_raw_note_path(path)
+ StateManager.set_raw_note_content(content)
+
+ # Set save folder
+ if save_folder:
+ resolved_save_folder = resolve_path(save_folder)
+ StateManager.set_save_folder_path(resolved_save_folder)
+ else:
+ default_folder = Config.get_freshness_folder(path)
+ StateManager.set_save_folder_path(default_folder)
+
+ # Check for existing metadata
+ if metadata and metadata.info_keyword and metadata.info_query:
+ StateManager.set_metadata(metadata)
+ StateManager.set_info_keyword(metadata.info_keyword)
+ StateManager.set_info_query(metadata.info_query)
+ StateManager.set_step(StateManager.STEP_METADATA_CONFIRMED)
+ render_success("기존 메타데이터를 발견했습니다. 검색 단계로 진행합니다.")
+ else:
+ StateManager.set_step(StateManager.STEP_NOTE_VALIDATED)
+ render_success(f"노트가 확인되었습니다: {path.name}")
+
+ st.rerun()
+
+
+def handle_extraction(schema_content: str):
+ """Handle information extraction from the note."""
+ note_path = StateManager.get_raw_note_path()
+ if not note_path:
+ render_error("노트 경로를 찾을 수 없습니다.")
+ return
+
+ # Ensure pandoc is installed before using it
+ if not ensure_pandoc_installed():
+ render_error("Pandoc이 필요합니다. 설치 후 다시 시도해주세요.")
+ return
+
+ with st.spinner("노트에서 키워드와 쿼리를 추출 중..."):
+ try:
+ # Convert markdown to docx using pypandoc
+ with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_file:
+ tmp_path = Path(tmp_file.name)
+
+ pypandoc.convert_file(str(note_path), "docx", outputfile=str(tmp_path))
+
+ # Call Upstage Information Extraction API
+ client = UpstageClient()
+ result = client.extract_information(tmp_path, schema_content)
+
+ # Clean up temp file
+ tmp_path.unlink()
+
+ if not result:
+ render_error("정보 추출에 실패했습니다.")
+ return
+
+ # Parse results
+ info_keyword, info_query = ResponseParser.parse_extraction_result(result)
+
+ if not info_keyword and not info_query:
+ render_error(
+ "키워드와 쿼리를 추출하지 못했습니다. 템플릿을 확인해주세요."
+ )
+ return
+
+ # Save to state
+ StateManager.set_info_keyword(info_keyword)
+ StateManager.set_info_query(info_query)
+ StateManager.set_step(StateManager.STEP_EXTRACTION_DONE)
+
+ render_success(
+ f"추출 완료: {len(info_keyword)}개 키워드, {len(info_query)}개 쿼리"
+ )
+ st.rerun()
+
+ except Exception as e:
+ render_error(f"추출 중 오류가 발생했습니다: {str(e)}")
+
+
+def handle_metadata_confirmation(keywords: list, queries: list):
+ """Handle confirmation of extracted metadata."""
+ note_path = StateManager.get_raw_note_path()
+
+ # Update note with metadata
+ success = FileHandler.update_note_metadata(
+ note_path, info_keyword=keywords, info_query=queries
+ )
+
+ if success:
+ StateManager.set_info_keyword(keywords)
+ StateManager.set_info_query(queries)
+ StateManager.set_step(StateManager.STEP_METADATA_CONFIRMED)
+ render_success("메타데이터가 노트에 저장되었습니다.")
+ st.rerun()
+ else:
+ render_error("메타데이터 저장에 실패했습니다.")
+
+
+def handle_search():
+ """Handle Wikipedia and Tavily searches."""
+ keywords = StateManager.get_info_keyword()
+ queries = StateManager.get_info_query()
+ save_folder = StateManager.get_save_folder_path()
+ note_path = StateManager.get_raw_note_path()
+
+ wiki_results = []
+ tavily_results = []
+
+ # Wikipedia search
+ if keywords:
+ with st.spinner("Wikipedia 검색 중..."):
+ wiki_client = WikipediaClient(language="ko")
+ for keyword in keywords:
+ result = wiki_client.search_and_get_summary(keyword)
+ if result and result.get("wiki_exists", False):
+ wiki_results.append(result)
+ print(
+ f"Wikipedia 결과: {keyword} -> wiki_exists={result.get('wiki_exists')}"
+ )
+
+ # Save wiki results
+ if wiki_results:
+ wiki_content = "# Wikipedia 검색 결과\n\n"
+ for r in wiki_results:
+ wiki_content += f"## {r['title']} ({r['keyword']})\n\n"
+ wiki_content += f"{r['summary']}\n\n"
+ wiki_content += f"[Wikipedia 링크]({r['url']})\n\n---\n\n"
+
+ print(f"save_folder: {save_folder}")
+ FileHandler.save_search_result(save_folder, "wiki_search", wiki_content)
+
+ # Update note with search timestamp
+ timestamp = (
+ wiki_results[0]["searched_at"]
+ if wiki_results
+ else FileHandler.get_current_timestamp()
+ )
+ FileHandler.update_note_metadata(note_path, wiki_searched_at=timestamp)
+
+ # Tavily search
+ if queries and Config.validate_tavily():
+ with st.spinner("Tavily 검색 중..."):
+ try:
+ tavily_client = TavilyClient()
+ for query in queries:
+ result = tavily_client.search_and_parse(query)
+ if result:
+ tavily_results.append(result)
+
+ # Save tavily results
+ if tavily_results:
+ tavily_content = "# Tavily 검색 결과\n\n"
+ for r in tavily_results:
+ tavily_content += f"## 쿼리: {r['query']}\n\n"
+ for item in r["results"]:
+ tavily_content += f"### {item['title']}\n\n"
+ tavily_content += f"{item['content']}\n\n"
+ tavily_content += f"[원본 링크]({item['url']})\n\n"
+ tavily_content += "---\n\n"
+
+ FileHandler.save_search_result(
+ save_folder, "tavily_search", tavily_content
+ )
+
+ # Update note with search timestamp
+ timestamp = (
+ tavily_results[0]["searched_at"]
+ if tavily_results
+ else FileHandler.get_current_timestamp()
+ )
+ FileHandler.update_note_metadata(
+ note_path, tavily_searched_at=timestamp
+ )
+ except ValueError as e:
+ render_info(f"Tavily 검색을 건너뜁니다: {str(e)}")
+ elif queries:
+ render_info("Tavily API 키가 설정되지 않아 검색을 건너뜁니다.")
+
+ # Save results to state
+ StateManager.set_wiki_results(wiki_results)
+ StateManager.set_tavily_results(tavily_results)
+ StateManager.set_step(StateManager.STEP_SEARCH_DONE)
+
+ render_success("검색이 완료되었습니다.")
+ st.rerun()
+
+
+def handle_guide_generation():
+ """Handle freshness guide generation."""
+ wiki_results = StateManager.get_wiki_results()
+ tavily_results = StateManager.get_tavily_results()
+ note_content = StateManager.get_raw_note_content()
+ save_folder = StateManager.get_save_folder_path()
+ note_path = StateManager.get_raw_note_path()
+
+ full_guide = "# 최신성 검토 가이드\n\n"
+
+ client = UpstageClient()
+ loader = PromptLoader(prompts_dir=PROMPTS_DIR)
+
+ # Generate guide from Wikipedia results
+ if wiki_results:
+ with st.spinner("Wikipedia 기반 가이드 생성 중..."):
+ full_guide += "## Wikipedia 기반 검토\n\n"
+
+ # Load wiki template
+ wiki_template = loader.load_template("ck_recentness_wiki")
+
+ for result in wiki_results:
+ if wiki_template:
+ user_vars = {
+ "keyword": result["keyword"],
+ "wiki_title": result["title"],
+ "wiki_summary": result["summary"],
+ "note_content": note_content[:3000],
+ }
+ user_prompt = wiki_template.format_user_prompt(**user_vars)
+ guide = client.generate_freshness_guide(
+ wiki_template.system_prompt, user_prompt
+ )
+ else:
+ # Fallback if template not found
+ guide = None
+
+ if guide:
+ full_guide += f"### {result['keyword']}\n\n{guide}\n\n---\n\n"
+
+ # Generate guide from Tavily results
+ if tavily_results:
+ with st.spinner("Tavily 기반 가이드 생성 중..."):
+ full_guide += "## 웹 검색 기반 검토\n\n"
+
+ # Load tavily template
+ tavily_template = loader.load_template("ck_recentness_tavily")
+
+ for result in tavily_results:
+ search_results_text = ""
+ for item in result["results"]:
+ search_results_text += f"### {item['title']}\n{item['content']}\n\n"
+
+ if tavily_template:
+ user_vars = {
+ "query": result["query"],
+ "search_results": search_results_text,
+ "note_content": note_content[:3000],
+ }
+ user_prompt = tavily_template.format_user_prompt(**user_vars)
+ guide = client.generate_freshness_guide(
+ tavily_template.system_prompt, user_prompt
+ )
+ else:
+ guide = None
+
+ if guide:
+ full_guide += f"### {result['query']}\n\n{guide}\n\n---\n\n"
+
+ # Save full guide
+ FileHandler.save_search_result(save_folder, "rcnt-guide-full", full_guide)
+
+ # Generate summary
+ with st.spinner("요약 생성 중..."):
+ summary_template = loader.load_template("ck_recentness_summary")
+
+ if summary_template:
+ user_vars = {"full_guide": full_guide[:2000]}
+ summary_prompt = summary_template.format_user_prompt(**user_vars)
+ summary = client.generate_freshness_guide(
+ summary_template.system_prompt, summary_prompt
+ )
+ else:
+ summary = None
+
+ if summary:
+ # Get relative path for backlink
+ note_stem = note_path.stem
+ guide_path = f"{note_stem}/rcnt-guide-full"
+
+ # Insert guide summary into note
+ FileHandler.insert_freshness_guide(note_path, summary, guide_path)
+
+ StateManager.set_step(StateManager.STEP_GUIDE_GENERATED)
+ render_success("최신성 검토 가이드가 생성되었습니다!")
+ st.rerun()
+
+
+def main():
+ """Main application entry point."""
+ initialize_app()
+
+ # Title
+ st.title("🔄 최신 정보 확인")
+ st.caption(
+ "노트의 정보가 최신인지 확인하고, 최신성 검토 가이드를 노트에 추가합니다!"
+ )
+
+ # Check API key
+ if not validate_api_key():
+ return
+
+ # Check pandoc installation
+ try:
+ pypandoc.get_pandoc_path()
+ pandoc_available = True
+ except (OSError, RuntimeError):
+ pandoc_available = False
+
+ if not pandoc_available:
+ st.warning("⚠️ Pandoc이 설치되지 않았습니다. 최신성 검토를 위해 필요합니다.")
+
+ st.markdown("### 설치 방법")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown("##### macOS")
+ st.markdown("**방법 1:** Homebrew 사용 (추천)")
+ st.code("brew install pandoc")
+ st.markdown(
+ "**방법 2:** [공식 인스톨러 다운로드](https://github.com/jgm/pandoc/releases/latest)"
+ )
+
+ with col2:
+ st.markdown("##### Windows")
+ st.markdown("**방법 1:** winget 사용 (추천)")
+ st.code("winget install --source winget --exact --id JohnMacFarlane.Pandoc")
+ st.markdown(
+ "**방법 2:** [공식 인스톨러 다운로드](https://github.com/jgm/pandoc/releases/latest)"
+ )
+
+ st.info("💡 설치 후 새로고침을 해주세요.")
+ st.stop()
+
+ # Main content based on current step
+ current_step = StateManager.get_current_step()
+
+ # Step 1: Initial Setup & Validation
+ if current_step == StateManager.STEP_INIT:
+ note_path, save_folder = render_file_input_section()
+
+ if st.button("노트 검증", type="primary"):
+ if note_path:
+ handle_note_validation(note_path, save_folder)
+ else:
+ render_error("노트 경로를 입력해주세요.")
+
+ # Step 2: Template Selection & Extraction
+ elif current_step == StateManager.STEP_NOTE_VALIDATED:
+ st.markdown("---")
+ default_schema = get_default_schema()
+ schema_content = render_template_selection_section(default_schema)
+
+ if st.button("템플릿 선택 완료", type="primary"):
+ handle_extraction(schema_content)
+
+ # Step 3: Metadata Review
+ elif current_step == StateManager.STEP_EXTRACTION_DONE:
+ st.markdown("---")
+ keywords = StateManager.get_info_keyword()
+ queries = StateManager.get_info_query()
+
+ edited_keywords, edited_queries = render_metadata_review_section(
+ keywords, queries
+ )
+
+ if st.button("최신성 메타데이터 확정", type="primary"):
+ handle_metadata_confirmation(edited_keywords, edited_queries)
+
+ # Step 4: Search
+ elif current_step == StateManager.STEP_METADATA_CONFIRMED:
+ st.markdown("---")
+ st.markdown("## 4. 검색 실행")
+
+ keywords = StateManager.get_info_keyword()
+ queries = StateManager.get_info_query()
+
+ st.markdown(f"**검색할 키워드:** {', '.join(keywords)}")
+ st.markdown(f"**검색할 쿼리:** {', '.join(queries)}")
+
+ if st.button("검색 시작", type="primary"):
+ handle_search()
+
+ # Step 5: Guide Generation
+ elif current_step == StateManager.STEP_SEARCH_DONE:
+ st.markdown("---")
+ wiki_results = StateManager.get_wiki_results()
+ tavily_results = StateManager.get_tavily_results()
+
+ render_search_results_section(wiki_results, tavily_results)
+
+ st.markdown("---")
+ if st.button("최신성 가이드 생성", type="primary"):
+ handle_guide_generation()
+
+ # Step 6: Completion
+ elif current_step == StateManager.STEP_GUIDE_GENERATED:
+ st.markdown("---")
+ st.success("✅ 최신성 검토가 완료되었습니다!")
+
+ save_folder = StateManager.get_save_folder_path()
+ note_path = StateManager.get_raw_note_path()
+
+ save_folder_display = format_path_for_display(
+ save_folder, prefer_windows_format=True
+ )
+ note_display = format_path_for_display(note_path, prefer_windows_format=True)
+
+ st.markdown(f"**검색 결과 저장 위치:** `{save_folder_display}`")
+ st.markdown(f"**업데이트된 노트:** `{note_display}`")
+
+ st.markdown("---")
+ st.markdown("### 생성된 파일")
+ st.markdown("- `wiki_search.md`: Wikipedia 검색 결과")
+ st.markdown("- `tavily_search.md`: Tavily 검색 결과")
+ st.markdown("- `rcnt-guide-full.md`: 전체 최신성 검토 가이드")
+
+ st.markdown("---")
+ if st.button("🔄 초기화", type="primary"):
+ StateManager.reset()
+ st.rerun()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/usecase/solar-knowledge-management/frontend/note_split.py b/usecase/solar-knowledge-management/frontend/note_split.py
new file mode 100644
index 0000000..a7fd92e
--- /dev/null
+++ b/usecase/solar-knowledge-management/frontend/note_split.py
@@ -0,0 +1,499 @@
+"""Main Streamlit application for Atomic Note Weaver."""
+import streamlit as st
+import asyncio
+import sys
+from pathlib import Path
+from typing import List, Optional
+
+# Add project root to path
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root))
+
+# Define prompts directory
+PROMPTS_DIR = project_root / 'prompts'
+
+from backend.note_split.config import Config
+from backend.note_split.models import Topic
+from backend.note_split.core.state_manager import StateManager
+from backend.note_split.core.file_handler import FileHandler
+from backend.note_split.core.path_utils import resolve_path, format_path_for_display
+from backend.note_split.llm.client import UpstageClient
+from backend.note_split.llm.parsers import ResponseParser
+from backend.note_split.llm.prompt_loader import PromptLoader
+from backend.note_split.ui.components import (
+ render_file_input_section,
+ render_template_selection_section,
+ render_topics_list,
+ render_batch_actions,
+ render_add_topics_form,
+ render_error,
+ render_success,
+ render_info
+)
+
+
+def initialize_app():
+ """Initialize the application."""
+ Config.ensure_directories()
+ StateManager.initialize()
+
+
+def validate_api_key() -> bool:
+ """Validate that API key is configured."""
+ if not Config.validate():
+ st.error(
+ "⚠️ Upstage API key not found. Please set UPSTAGE_API_KEY in your .env file."
+ )
+ st.code("UPSTAGE_API_KEY=your_api_key_here")
+ return False
+ return True
+
+
+def handle_note_analysis(note_path: str, save_folder: str, instructions: str):
+ """Handle the note analysis step."""
+ # Normalize path for WSL if needed
+ path = resolve_path(note_path)
+
+ if not path.exists():
+ render_error(f"File not found: {note_path}")
+ return
+
+ if not path.suffix == '.md':
+ render_error("Please provide a markdown (.md) file.")
+ return
+
+ # Read the note
+ content, lines = FileHandler.read_note(path)
+ if content is None:
+ render_error("Failed to read the note file.")
+ return
+
+ # Set paths in state
+ StateManager.set_raw_note_path(path)
+ StateManager.set_raw_note_content(content, lines)
+ StateManager.set_analysis_instructions(instructions)
+
+ # Set save folder
+ if save_folder:
+ resolved_save_folder = resolve_path(save_folder)
+ StateManager.set_save_folder_path(resolved_save_folder)
+ else:
+ default_folder = Config.get_atomic_notes_folder(path)
+ StateManager.set_save_folder_path(default_folder)
+
+ # Move to next step
+ StateManager.set_step(StateManager.STEP_TEMPLATE_SELECT)
+ render_success(f"Note loaded successfully: {path.name}")
+ st.rerun()
+
+
+def handle_template_selection(template_name: str):
+ """Handle template selection."""
+ StateManager.set_selected_template(template_name)
+ render_success(f"Template selected: {template_name}")
+
+
+def handle_topic_extraction():
+ """Handle topic extraction from the note."""
+ template_name = StateManager.get_selected_template()
+ if not template_name:
+ render_error("Please select a template first.")
+ return
+
+ # Load template
+ loader = PromptLoader(prompts_dir=PROMPTS_DIR)
+ template = loader.load_template(template_name)
+ if not template:
+ # 파일이 존재하는지 확인하여 더 구체적인 메시지 제공
+ template_path = loader.prompts_dir / f"{template_name}.yml"
+ if template_path.exists():
+ render_error(f"Failed to load template '{template_name}': The YAML file is empty or invalid. Please check the file format.")
+ else:
+ render_error(f"Template '{template_name}' not found. Please check the template name.")
+ return
+
+ # Get note content
+ content = StateManager.get_raw_note_content()
+ instructions = StateManager.get_analysis_instructions()
+
+ # Prepare variables for prompt
+ user_vars = {
+ 'note_content': content,
+ 'additional_instructions': instructions if instructions else 'None'
+ }
+
+ # Call LLM
+ with st.spinner("Extracting topics from your note..."):
+ try:
+ client = UpstageClient()
+ response = client.generate_with_template_sync(template, user_vars)
+
+ if not response:
+ render_error("Failed to get response from LLM.")
+ return
+
+ # Parse topics
+ topics = ResponseParser.parse_topics_from_json(response)
+
+ if not topics:
+ render_error("No topics were extracted. Please try again with different instructions.")
+ return
+
+ # Save topics to state
+ StateManager.set_topics(topics)
+ StateManager.set_step(StateManager.STEP_TOPICS_EXTRACTED)
+ render_success(f"Successfully extracted {len(topics)} topic(s)!")
+ st.rerun()
+
+ except Exception as e:
+ render_error(f"Error during topic extraction: {str(e)}")
+
+
+def handle_topic_update(index: int, updated_topic: Topic):
+ """Handle topic update."""
+ # Check if we need to update line numbers
+ original_topic = StateManager.get_topics()[index]
+
+ needs_line_update = (
+ updated_topic.topic != original_topic.topic or
+ updated_topic.coverage != original_topic.coverage
+ )
+
+ # If only use_llm changed, skip line number update and success message
+ only_llm_changed = (
+ updated_topic.topic == original_topic.topic and
+ updated_topic.coverage == original_topic.coverage and
+ updated_topic.use_llm != original_topic.use_llm
+ )
+
+ if needs_line_update:
+ # Load line number update template
+ loader = PromptLoader(prompts_dir=PROMPTS_DIR)
+ template = loader.load_template('line_number_update')
+
+ if template:
+ content = StateManager.get_raw_note_content()
+ user_vars = {
+ 'note_content': content,
+ 'topic': updated_topic.topic,
+ 'coverage': updated_topic.coverage
+ }
+
+ with st.spinner("Updating line numbers..."):
+ try:
+ client = UpstageClient()
+ response = client.generate_with_template_sync(template, user_vars)
+
+ if response:
+ new_line_numbers = ResponseParser.parse_line_numbers_from_json(response)
+ updated_topic.line_numbers = new_line_numbers
+
+ except Exception as e:
+ st.warning(f"Could not update line numbers automatically: {str(e)}")
+
+ # Update the topic
+ StateManager.update_topic(index, updated_topic)
+
+ if not only_llm_changed:
+ render_success("Topic updated successfully!")
+ else:
+ st.rerun()
+
+
+def handle_topic_delete(index: int):
+ """Handle topic deletion."""
+ StateManager.delete_topic(index)
+ render_success("Topic deleted successfully!")
+ st.rerun()
+
+
+def handle_topic_selection(index: int, selected: bool):
+ """Handle topic selection toggle."""
+ topics = StateManager.get_topics()
+ topics[index].selected = selected
+ StateManager.set_topics(topics)
+
+
+def handle_add_topics(guidance: str, num_topics: int, template_name: str):
+ """Handle adding new topics."""
+ loader = PromptLoader(prompts_dir=PROMPTS_DIR)
+ template = loader.load_template(template_name)
+
+ if not template:
+ # 파일이 존재하는지 확인하여 더 구체적인 메시지 제공
+ template_path = loader.prompts_dir / f"{template_name}.yml"
+ if template_path.exists():
+ render_error(f"Failed to load template '{template_name}': The YAML file is empty or invalid. Please check the file format.")
+ else:
+ render_error(f"Template '{template_name}' not found. Please check the template name.")
+ return
+
+ content = StateManager.get_raw_note_content()
+ existing_topics = StateManager.get_topics()
+ existing_topics_str = '\n'.join([f"- {t.topic}: {t.coverage}" for t in existing_topics])
+
+ user_vars = {
+ 'note_content': content,
+ 'existing_topics': existing_topics_str,
+ 'guidance': guidance if guidance else 'Extract additional relevant topics',
+ 'num_topics': num_topics
+ }
+
+ with st.spinner(f"Generating {num_topics} new topic(s)..."):
+ try:
+ client = UpstageClient()
+ response = client.generate_with_template_sync(template, user_vars)
+
+ if not response:
+ render_error("Failed to get response from LLM.")
+ return
+
+ new_topics = ResponseParser.parse_topics_from_json(response)
+
+ if not new_topics:
+ render_error("No new topics were generated.")
+ return
+
+ # Add new topics to state
+ for topic in new_topics:
+ StateManager.add_topic(topic)
+
+ render_success(f"Added {len(new_topics)} new topic(s)!")
+ st.rerun()
+
+ except Exception as e:
+ render_error(f"Error generating new topics: {str(e)}")
+
+
+async def generate_atomic_note(
+ topic: Topic,
+ lines: List[str],
+ save_folder: Path
+) -> bool:
+ """Generate and save a single atomic note.
+
+ Args:
+ topic: Topic object
+ lines: Lines from the raw note
+ save_folder: Folder to save the atomic note
+
+ Returns:
+ True if successful, False otherwise
+ """
+ # Get related content
+ related_content = FileHandler.get_lines_content(lines, topic.line_numbers)
+
+ # Generate content with LLM if use_llm is enabled
+ generated_content = None
+ if topic.use_llm:
+ try:
+ loader = PromptLoader(prompts_dir=PROMPTS_DIR)
+ template = loader.load_template('atomic_note_generate')
+
+ if template:
+ # Use user_direction if provided, otherwise use DEFAULT_ATOM_DIRECTION
+ direction = topic.user_direction or Config.DEFAULT_ATOM_DIRECTION
+
+ # If no direction is provided at all, skip LLM generation
+ if not direction:
+ print(f"Warning: No direction provided for {topic.topic}. Skipping LLM generation.")
+ else:
+ user_vars = {
+ 'topic': topic.topic,
+ 'coverage': topic.coverage,
+ 'related_content': related_content,
+ 'keywords': ', '.join(topic.keywords),
+ 'user_direction': direction
+ }
+
+ client = UpstageClient()
+ response = await client.generate_with_template(template, user_vars)
+
+ if response:
+ generated_content = ResponseParser.parse_atomic_note_content(response)
+
+ except Exception as e:
+ print(f"Warning: Could not generate content for {topic.topic}: {e}")
+
+ # Create atomic note content
+ note_content = FileHandler.create_atomic_note(
+ topic,
+ related_content,
+ generated_content
+ )
+
+ # Save the note
+ return FileHandler.save_atomic_note(save_folder, topic, note_content)
+
+
+async def handle_generate_atomic_notes_async(topics: List[Topic]):
+ """Handle atomic note generation (async)."""
+ lines = StateManager.get_raw_note_lines()
+ save_folder = StateManager.get_save_folder_path()
+ raw_note_path = StateManager.get_raw_note_path()
+
+ if not lines or not save_folder or not raw_note_path:
+ render_error("Missing required data. Please start over.")
+ return
+
+ # Generate all atomic notes in parallel
+ progress_bar = st.progress(0, text="Generating atomic notes...")
+
+ tasks = [generate_atomic_note(topic, lines, save_folder) for topic in topics]
+ results = []
+
+ for i, task in enumerate(asyncio.as_completed(tasks)):
+ result = await task
+ results.append(result)
+ progress = (i + 1) / len(tasks)
+ progress_bar.progress(progress, text=f"Generated {i + 1}/{len(tasks)} atomic notes...")
+
+ success_count = sum(results)
+
+ if success_count == 0:
+ render_error("Failed to generate any atomic notes.")
+ return
+
+ # Insert backlinks in raw note
+ modified_lines = FileHandler.insert_backlinks(lines, topics)
+
+ # Append topic list to raw note
+ modified_lines = FileHandler.append_topic_list(modified_lines, topics)
+
+ # Save modified raw note
+ if FileHandler.save_raw_note(raw_note_path, modified_lines):
+ render_success(
+ f"Successfully generated {success_count}/{len(topics)} atomic note(s)!\n\n"
+ f"Saved to: {save_folder}"
+ )
+ StateManager.set_step(StateManager.STEP_NOTES_GENERATED)
+ else:
+ render_error("Failed to update the raw note with backlinks.")
+
+
+def handle_generate_atomic_notes():
+ """Handle atomic note generation (sync wrapper)."""
+ selected_topics = StateManager.get_selected_topics()
+
+ if not selected_topics:
+ render_error("Please select at least one topic to generate atomic notes.")
+ return
+
+ # Run async function
+ asyncio.run(handle_generate_atomic_notes_async(selected_topics))
+
+
+def main():
+ """Main application entry point."""
+ initialize_app()
+
+ # Title
+ st.title("📝 노트 분할")
+ st.caption("원시 노트를 원자적이고 상호 연결된 지식으로 변환합니다.")
+
+ # Check API key
+ if not validate_api_key():
+ return
+
+ # Sidebar
+ with st.sidebar:
+ st.markdown("## Navigation")
+ current_step = StateManager.get_current_step()
+
+ st.markdown(f"**Current Step:** {current_step}")
+
+ if st.button("🔄 Reset Application"):
+ StateManager.reset()
+ st.rerun()
+
+ st.markdown("---")
+ st.markdown("### About")
+ st.markdown(
+ "이 애플리케이션은 AI 기반 토픽 추출을 사용하여 "
+ "큰 노트를 원자 노트로 분해하는 데 도움을 줍니다."
+ )
+
+ # Main content
+ current_step = StateManager.get_current_step()
+
+ # Step 1: Initial Setup
+ if current_step == StateManager.STEP_INIT:
+ note_path, save_folder, instructions = render_file_input_section()
+
+ if st.button("Start Analysis", type="primary"):
+ if note_path:
+ handle_note_analysis(note_path, save_folder, instructions)
+ else:
+ render_error("Please provide a note path.")
+
+ # Step 2: Template Selection
+ elif current_step == StateManager.STEP_TEMPLATE_SELECT:
+ st.markdown("---")
+ loader = PromptLoader(prompts_dir=PROMPTS_DIR)
+ templates = loader.get_templates_info()
+
+ selected_template = render_template_selection_section(templates)
+
+ if selected_template:
+ handle_template_selection(selected_template)
+
+ if st.button("Extract Topics", type="primary"):
+ handle_topic_extraction()
+
+ # Step 3: Topic Management
+ elif current_step in [StateManager.STEP_TOPICS_EXTRACTED, StateManager.STEP_NOTES_GENERATED]:
+ st.markdown("---")
+ st.markdown("## 3. Review and Manage Topics")
+
+ # Batch actions
+ render_batch_actions()
+
+ st.markdown("---")
+
+ # Topics list
+ topics = StateManager.get_topics()
+ render_topics_list(
+ topics,
+ on_update=handle_topic_update,
+ on_delete=handle_topic_delete,
+ on_select=handle_topic_selection
+ )
+
+ st.markdown("---")
+
+ # Add topics form
+ render_add_topics_form(on_add=handle_add_topics)
+
+ st.markdown("---")
+
+ # Generate atomic notes button
+ st.markdown("## 4. Generate Atomic Notes")
+
+ selected_topics = StateManager.get_selected_topics()
+
+ if selected_topics:
+ st.info(f"Ready to generate {len(selected_topics)} atomic note(s).")
+
+ if st.button("Generate Atomic Notes", type="primary", use_container_width=True):
+ handle_generate_atomic_notes()
+ else:
+ st.warning("Please select at least one topic to generate atomic notes.")
+
+ # Show results if notes were generated
+ if current_step == StateManager.STEP_NOTES_GENERATED:
+ st.markdown("---")
+ st.success("✅ Atomic notes have been generated!")
+
+ save_folder = StateManager.get_save_folder_path()
+ raw_note_path = StateManager.get_raw_note_path()
+
+ # Format paths for display (Windows format if in WSL)
+ save_folder_display = format_path_for_display(save_folder, prefer_windows_format=True) if save_folder else None
+ raw_note_display = format_path_for_display(raw_note_path, prefer_windows_format=True) if raw_note_path else None
+
+ st.markdown(f"**Atomic notes saved to:** `{save_folder_display}`")
+ st.markdown(f"**Raw note updated:** `{raw_note_display}`")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/usecase/solar-knowledge-management/frontend/related_note.py b/usecase/solar-knowledge-management/frontend/related_note.py
new file mode 100644
index 0000000..33db646
--- /dev/null
+++ b/usecase/solar-knowledge-management/frontend/related_note.py
@@ -0,0 +1,156 @@
+"""
+연관 노트 추천 Streamlit 앱
+"""
+
+import sys
+from pathlib import Path
+import streamlit as st
+
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root))
+
+from backend.related_note import Related_Note
+
+
+@st.cache_resource
+def get_engine(vault_path: str):
+ """엔진 인스턴스를 캐싱하여 재사용 (Chroma DB 연결 충돌 방지)"""
+ return Related_Note(vault_path=vault_path)
+
+
+def init_session_state():
+ """세션 상태 초기화"""
+ if "show_input" not in st.session_state:
+ st.session_state.show_input = False
+
+
+def render_embedding_section(engine):
+ """임베딩 섹션 렌더링"""
+ notes_to_embed = engine.get_unembedded_notes()
+
+ st.warning("🌀 아직 임베딩되지 않은 노트가 있습니다.")
+ st.write(f"총 {len(notes_to_embed)}개 노트가 임베딩 대상입니다:")
+
+ with st.expander("📄 임베딩 대상 노트 목록 보기"):
+ for note in notes_to_embed:
+ st.text(f"- {note}")
+
+ if st.button("임베딩 시작하기 🚀"):
+ with st.spinner("노트 임베딩 중입니다... 시간이 조금 걸릴 수 있습니다."):
+ engine.index_unembedded_notes()
+
+ st.success("✅ 임베딩이 완료되었습니다!")
+ st.balloons()
+ st.rerun()
+
+
+def render_recommendation_section(engine):
+ """추천 섹션 렌더링"""
+ st.success("🎉 모든 노트가 이미 임베딩되었습니다!")
+ st.write("바로 추천 노트를 생성할 수 있습니다.")
+
+ # 단계별 UI
+ if not st.session_state.show_input:
+ # STEP 1: 노트 경로 입력 버튼
+ if st.button("노트 경로 입력", type="primary"):
+ st.session_state.show_input = True
+ st.rerun()
+ else:
+ # STEP 2: 텍스트 입력 및 추천 결과
+ target_note = st.text_input(
+ "추천을 받을 노트 경로를 입력 후 Enter를 눌러주세요.",
+ key="target_note_input",
+ value=st.session_state.get("last_target_note", ""),
+ )
+
+ if target_note:
+ # 추천 결과가 세션에 없으면 새로 생성
+ if (
+ "related_results" not in st.session_state
+ or st.session_state.get("last_target_note") != target_note
+ ):
+ with st.spinner("연관 노트를 찾는 중입니다..."):
+ related = engine.append_related_links(target_note, k=3)
+ st.session_state.related_results = related
+ st.session_state.last_target_note = target_note
+
+ # 추천 결과가 있으면 표시 (입력 여부와 무관)
+ if "related_results" in st.session_state and st.session_state.related_results:
+ related = st.session_state.related_results
+
+ st.subheader("🔗 추천 노트 3개")
+ for r in related:
+ st.markdown(r)
+
+ # 새로고침 버튼
+ st.text("")
+ *_, reset_btn = st.columns([5, 1])
+ with reset_btn:
+ if st.button(
+ "🔄ㅤ새로고침",
+ use_container_width=True,
+ help="처음 단계로 돌아갑니다",
+ ):
+ # 연관 노트 페이지 관련 키 초기화
+ st.session_state.show_input = False
+ keys_to_delete = [
+ "target_note_input",
+ "related_results",
+ "last_target_note",
+ ]
+ for key in keys_to_delete:
+ if key in st.session_state:
+ del st.session_state[key]
+ st.rerun()
+ elif target_note:
+ st.info("연관된 노트를 찾지 못했습니다.")
+
+
+def main():
+ """메인 함수"""
+ # 세션 상태 초기화
+ init_session_state()
+
+ # 메인 헤더
+ st.title("📝 연관 노트 추천")
+ st.caption("업로드한 노트와 관련성 높은 내용을 가진 노트들을 추천받아 보세요!")
+ st.text("")
+
+ # Vault 경로 확인
+ vault_path = st.session_state.get("vault_path", "")
+
+ if not vault_path:
+ st.warning("👈 왼쪽 사이드바에서 ***Vault 경로*** 를 입력해주세요.")
+ st.stop()
+
+ # 경로 유효성 검사
+ vault_dir = Path(vault_path)
+ if not vault_dir.exists() or not vault_dir.is_dir():
+ st.error(f"❌ 유효하지 않은 경로입니다: {vault_path}")
+ st.stop()
+
+ # 엔진 초기화 (캐싱됨)
+ try:
+ engine = get_engine(vault_path=vault_path)
+ st.success(
+ f"""✅ Vault 연결 완료: {vault_path}
+
+(Vault 경로의 변경이 필요한 경우 왼쪽 사이드바에서 수정해 주세요.)"""
+ )
+ except Exception as e:
+ st.error(f"❌ 엔진 초기화 실패: {e}")
+ st.stop()
+
+ # 임베딩 안 된 노트 확인
+ notes_to_embed = engine.get_unembedded_notes()
+
+ if not notes_to_embed:
+ # 모든 노트가 임베딩된 경우: 추천 섹션
+ render_recommendation_section(engine)
+ else:
+ # 임베딩 안 된 노트가 있는 경우: 임베딩 섹션
+ render_embedding_section(engine)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/usecase/solar-knowledge-management/frontend/tag_suggest.py b/usecase/solar-knowledge-management/frontend/tag_suggest.py
new file mode 100644
index 0000000..1b015b4
--- /dev/null
+++ b/usecase/solar-knowledge-management/frontend/tag_suggest.py
@@ -0,0 +1,456 @@
+"""
+노트 태그 추천 Streamlit 앱
+"""
+
+import sys
+from pathlib import Path
+
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root))
+
+import re
+import time
+import traceback
+import streamlit as st
+
+from backend.tag_suggest import (
+ TagExtractor,
+ GuidelineGenerator,
+ ChecklistType,
+ TagGenerator,
+ TagComparator,
+ TagMatch,
+ add_yaml_frontmatter,
+)
+
+
+def init_session_state():
+ """세션 상태 초기화 (태그 추천 전용)"""
+ # vault_path와 uploaded_file은 공통 요소여서, frontend/app.py 에서 관리
+
+ # 체크리스트 (태그 생성 가이드라인)
+ if "checklist" not in st.session_state:
+ st.session_state.checklist = None
+ # 기존 태그
+ if "existing_tags" not in st.session_state:
+ st.session_state.existing_tags = []
+ # 신규 태그 (생성된 태그)
+ if "new_tags" not in st.session_state:
+ st.session_state.new_tags = []
+ # 태그 비교
+ if "matches" not in st.session_state:
+ st.session_state.matches = []
+
+ if "step" not in st.session_state:
+ st.session_state.step = 1
+
+
+def render_existing_tags_preview():
+ """기존 태그 수집 결과"""
+ vault_path_str = st.session_state.get("vault_path", "")
+ if not vault_path_str:
+ return
+
+ vault_path = Path(vault_path_str.strip())
+ if not vault_path.exists():
+ st.warning(f"⚠️ Vault 경로를 찾을 수 없습니다: {vault_path_str}")
+ return
+
+ # 기존 태그가 아직 로드되지 않았다면 로드
+ if not st.session_state.existing_tags:
+ with st.spinner("기존 태그를 수집하는 중..."):
+ try:
+ extractor = TagExtractor()
+ existing_tags = list(extractor.get_unique_tags(str(vault_path)))
+ st.session_state.existing_tags = existing_tags
+ except Exception as e:
+ st.error(f"❌ 태그 수집 실패: {e}")
+ return
+
+ existing_tags = st.session_state.existing_tags
+
+ # 결과 표시
+ with st.expander("📊 기존 태그 미리보기", expanded=False):
+ if existing_tags:
+ st.info(f"✓ 총 **{len(existing_tags)}개**의 고유 태그 발견")
+
+ # 빈도순으로 상위 10개 태그 표시
+ try:
+ extractor = TagExtractor()
+ tag_counts = extractor.count_tags(str(vault_path))
+
+ # 상위 10개 추출
+ top_10_tags = list(tag_counts.items())[:10]
+
+ st.markdown("**상위 10개 태그 (빈도순):**")
+ st.code(", ".join([tag for tag, _ in top_10_tags]))
+
+ if len(existing_tags) > 10:
+ st.caption(f"... 외 {len(existing_tags) - 10}개")
+ except Exception as e:
+ # 빈도 계산 실패 시 기존 방식으로 폴백
+ st.markdown("**태그 목록 (일부):**")
+ st.code(", ".join(sorted(existing_tags)[:10]))
+ if len(existing_tags) > 10:
+ st.caption(f"... 외 {len(existing_tags) - 10}개")
+ else:
+ st.warning("⚠️ 기존 태그가 없습니다. 모든 태그가 새로운 태그로 추가됩니다.")
+
+
+def render_checklist_form():
+ """체크리스트 설문 폼 렌더링"""
+ with st.container(border=True):
+ st.markdown("#### 📝 태그 작성 가이드라인")
+ col_lang, col_case = st.columns(2)
+ col_sep, col_num = st.columns(2)
+
+ # 주로 사용하는 언어
+ with col_lang:
+ st.markdown("**1/ 주로 사용하는 언어**")
+ language = st.radio(
+ "언어",
+ options=["en", "ko"],
+ format_func=lambda x: {
+ "en": "영어",
+ "ko": "한국어",
+ }[x],
+ label_visibility="collapsed",
+ key="language_radio",
+ )
+
+ # 대소문자 규칙 (영어 사용 시)
+ with col_case:
+ st.markdown("**2/ 영어 대소문자 규칙**")
+ case_style = None
+ if language in ["en"]:
+ case_style = st.radio(
+ "대소문자",
+ options=["lowercase", "uppercase"],
+ format_func=lambda x: {
+ "lowercase": "소문자 (e.g., `upstage`)",
+ "uppercase": "대문자 (e.g., `UPSTAGE`)",
+ }[x],
+ label_visibility="collapsed",
+ key="case_style_radio",
+ )
+
+ # 단어 구분자
+ with col_sep:
+ st.markdown("**3/ 단어 구분자**")
+ separator = st.radio(
+ "구분자",
+ options=["hyphen", "underscore"],
+ format_func=lambda x: {
+ "hyphen": "하이픈ㅤㅤ (e.g., `deep-learning`)",
+ "underscore": "언더스코어 (e.g., `deep_learning`)",
+ }[x],
+ label_visibility="collapsed",
+ key="separator_radio",
+ )
+
+ # 태그 개수
+ with col_num:
+ st.markdown("**4/ 태그 개수 범위** (최소 2개, 최대 10개)")
+ col_min, col_max = st.columns(2)
+ with col_min:
+ min_count = st.number_input(
+ "최소",
+ min_value=2,
+ max_value=10,
+ value=2,
+ key="min_count_input",
+ )
+ with col_max:
+ max_count = st.number_input(
+ "최대",
+ min_value=2,
+ max_value=10,
+ value=5,
+ key="max_count_input",
+ )
+
+ # 최소값이 최대값보다 크면 경고
+ if min_count > max_count:
+ warning_min = st.warning("⚠️ 최소값이 최대값보다 클 수 없습니다.")
+ time.sleep(1)
+ warning_min.empty()
+ # 최대값이 최소값보다 작으면 경고
+ elif max_count < min_count:
+ warning_max = st.warning("⚠️ 최대값이 최소값보다 작을 수 없습니다.")
+ time.sleep(1)
+ warning_max.empty()
+
+
+ # 체크리스트 생성 (버튼 클릭 -> 과정 실행)
+ _, guide_ok = st.columns(2)
+ with guide_ok:
+ st.markdown("")
+ # step 3 이상이면 비활성화 (이미 태그가 생성됨)
+ is_disabled = st.session_state.step >= 3
+ if st.button(
+ "✅ㅤ태그 생성",
+ use_container_width=True,
+ type="primary",
+ disabled=is_disabled,
+ ):
+ # 최소/최대 검증
+ if min_count > max_count:
+ error_min = st.error("❌ 최소값이 최대값보다 클 수 없습니다.")
+ time.sleep(1)
+ error_min.empty()
+ elif max_count < min_count:
+ error_max = st.error("❌ 최대값이 최소값보다 작을 수 없습니다.")
+ time.sleep(1)
+ error_max.empty()
+
+ checklist: ChecklistType = {
+ "language": language,
+ "separator": separator,
+ "tag_count_range": {"min": int(min_count), "max": int(max_count)},
+ }
+
+ if case_style:
+ checklist["case_style"] = case_style
+
+ try:
+ # 유효성 검사
+ guideline_gen = GuidelineGenerator(checklist)
+ st.session_state.checklist = checklist
+
+ # 업로드된 파일 확인
+ if not st.session_state.get("uploaded_file"):
+ st.error("⚠️ 마크다운 파일을 업로드해주세요.")
+ return
+
+ # 태그 생성 프로세스 시작
+ progress_bar = st.progress(0)
+ status_text = st.empty()
+
+ # 파일 내용 읽기
+ uploaded_file = st.session_state.uploaded_file
+ md_content = uploaded_file.getvalue().decode("utf-8")
+ filename = uploaded_file.name
+
+ # 1. 태그 생성
+ status_text.caption("[1/2] 신규 태그 생성 중 ...")
+ progress_bar.progress(30)
+
+ tag_gen = TagGenerator()
+
+ new_tags = tag_gen.generate_tags(
+ guideline_gen, md_content, filename
+ )
+
+ st.session_state.new_tags = new_tags
+ progress_bar.progress(60)
+
+ # 2. 기존 태그와 비교
+ status_text.caption("[2/2] 기존 태그와 비교 중 ...")
+ comparator = TagComparator()
+
+ matches = comparator.compare_tags(
+ new_tags, st.session_state.existing_tags
+ )
+ st.session_state.matches = matches
+
+ progress_bar.progress(100)
+ status_text.empty()
+ progress_bar.empty()
+
+ st.session_state.step = 3
+ st.rerun()
+
+ except ValueError as e:
+ error_valueerror = st.error(f"❌ 오류: {e}")
+ time.sleep(1)
+ error_valueerror.empty()
+ except Exception as e:
+ st.error(f"❌ 태그 생성 실패: {e}")
+ with st.expander("상세 오류 정보"):
+ st.code(traceback.format_exc())
+
+
+def render_compare_tags():
+ """기존, 신규 태그 결과 시각화"""
+ with st.container(border=True):
+ st.markdown("#### 📊 태그 비교 결과")
+
+ if not st.session_state.matches:
+ return
+ matches: list[TagMatch] = st.session_state.matches
+
+ # 통계
+ new_count = sum(1 for m in matches if m.is_new)
+ matched_count = len(matches) - new_count
+
+ col_new_tags, col_match_tags, col_existing_tags = st.columns(3)
+ with col_new_tags:
+ st.metric("신규 태그", f"{new_count}개")
+ with col_match_tags:
+ st.metric("매칭된 태그", f"{matched_count}개")
+ with col_existing_tags:
+ st.metric("총 태그", f"{len(matches)}개")
+
+ # 상세 결과
+ for match in matches:
+ if match.is_new:
+ st.success(f"신규 : `{match.new_tag}` (유사도: {match.similarity:.2f})")
+ else:
+ st.info(
+ f"매칭 : `{match.new_tag}`ㅤ→ㅤ`{match.matched_tag}` (유사도: {match.similarity:.2f})"
+ )
+
+ # 최종 태그 확인 버튼
+ st.text("")
+ _, col_final_btn = st.columns(2)
+ with col_final_btn:
+ if st.button(
+ "✨ㅤ최종 태그 제안", type="primary", use_container_width=True
+ ):
+ st.session_state.step = 4
+ st.rerun()
+
+ return matches
+
+
+def render_final_offer(matches):
+ """최종 태그 제안"""
+ # 저장 상태 메시지 표시 및 새로고침 버튼
+ save_msg_col, *_, reset_btn = st.columns([20, 1, 1, 3])
+
+ with save_msg_col:
+ # 저장 결과 메시지 표시
+ if st.session_state.get("save_success_msg"):
+ st.success(st.session_state.save_success_msg)
+ st.session_state.save_success_msg = None
+ elif st.session_state.get("save_error_msg"):
+ st.error(st.session_state.save_error_msg)
+ st.session_state.save_error_msg = None
+
+ with reset_btn:
+ if st.button(
+ "🔄ㅤ새로고침",
+ use_container_width=True,
+ help="기존 태그를 수집하는 단계로 돌아갑니다",
+ ):
+ # 태그 추천 페이지 관련 키들만 삭제
+ keys_to_delete = [
+ "step",
+ "checklist",
+ "existing_tags",
+ "new_tags",
+ "matches",
+ "save_success_msg",
+ "save_error_msg",
+ ]
+
+ for key in keys_to_delete:
+ if key in st.session_state:
+ del st.session_state[key]
+
+ init_session_state()
+ st.rerun()
+
+ with st.container(border=True):
+ st.markdown("#### ✨ 최종 태그 제안")
+
+ # 정적 메서드로 호출 (인스턴스 생성 불필요)
+ final_tags = TagComparator.get_final_tags(matches)
+
+ # YAML frontmatter가 추가된 파일 생성
+ uploaded_file = st.session_state.uploaded_file
+ original_content = uploaded_file.getvalue().decode("utf-8")
+ updated_content = add_yaml_frontmatter(original_content, final_tags)
+
+ # YAML frontmatter 미리보기 (첫 번째 --- 부터 두 번째 --- 까지)
+ yaml_match = re.match(r"(---\n.*?\n---)", updated_content, re.DOTALL)
+ if yaml_match:
+ yaml_preview = yaml_match.group(1)
+ st.code(yaml_preview, language="yaml")
+ else:
+ st.code(updated_content[:200], language="yaml") # fallback
+
+ # 저장 및 다운로드 버튼
+ st.text("")
+ *_, download_btn = st.columns(4)
+
+ with download_btn:
+ # Vault에 저장 버튼
+ vault_path = st.session_state.get("vault_path")
+ if vault_path and Path(vault_path).exists():
+ if st.button(
+ "💾ㅤVault에 저장", use_container_width=True, type="primary"
+ ):
+ try:
+ save_path = Path(vault_path) / uploaded_file.name
+ save_path.write_text(updated_content, encoding="utf-8")
+ st.session_state.save_success_msg = f"✅ 저장 완료: {save_path}"
+ st.rerun()
+ except Exception as e:
+ st.session_state.save_error_msg = f"❌ 저장 실패: {e}"
+ st.rerun()
+ else:
+ # Vault 경로가 없으면 다운로드 버튼
+ st.download_button(
+ label="⬇️ㅤ다운로드",
+ data=updated_content.encode("utf-8"),
+ file_name=uploaded_file.name,
+ mime="text/markdown",
+ use_container_width=True,
+ type="primary",
+ )
+ st.caption("💡 YAML frontmatter가 추가된 파일을 저장하세요")
+
+
+def main():
+ """메인 함수"""
+ # 세션 상태 초기화
+ init_session_state()
+
+ # 메인 헤더
+ st.title("🏷️ 태그 추천")
+ st.caption("노트에 적합한 태그를 Upstage Solar Pro 2로 추천받아 보세요!")
+ st.text("")
+
+ # 단계별 렌더링
+ # Step 1: 기존 태그 미리보기
+ if st.session_state.step == 1:
+ vault_path = st.session_state.get("vault_path")
+ uploaded_file = st.session_state.get("uploaded_file")
+
+ if vault_path and uploaded_file:
+ st.info(
+ f"- Vault 경로:ㅤ{vault_path}\n"
+ f"- Markdown 파일:ㅤ{uploaded_file.name}\n\n"
+ f"**💡 변경이 필요한 경우 왼쪽 사이드바에서 수정해 주세요.**"
+ )
+ if st.button("기존 태그 분석 시작", type="primary"):
+ st.session_state.step = 2
+ st.rerun()
+ else:
+ st.warning(
+ "👈ㅤ왼쪽 사이드바에서 ***Vault 경로*** 와 ***Markdown 파일 업로드*** 설정을 완료해 주세요."
+ )
+
+ # Step 2-3: 기존 태그 미리보기 + 태그 작성 가이드라인 + 태그 비교 결과
+ if st.session_state.step >= 2 and st.session_state.step < 4:
+ render_existing_tags_preview()
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ render_checklist_form()
+
+ with col2:
+ # Step 3: 태그 비교 결과
+ if st.session_state.step >= 3:
+ matches = render_compare_tags()
+
+ # Step 4: 최종 추천 태그만 표시
+ if st.session_state.step == 4:
+ matches = st.session_state.matches
+ render_final_offer(matches)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/usecase/solar-knowledge-management/prompts/atomic_note_generate.yml b/usecase/solar-knowledge-management/prompts/atomic_note_generate.yml
new file mode 100644
index 0000000..be1c8b6
--- /dev/null
+++ b/usecase/solar-knowledge-management/prompts/atomic_note_generate.yml
@@ -0,0 +1,62 @@
+name: atomic_note_generate
+description: Generate draft content and writing guidelines for an atomic note based on topic and user direction
+
+system_prompt: |
+ You are an expert at creating atomic notes - concise, focused notes on a single topic.
+
+ Your task is to generate TWO distinct sections for an atomic note:
+ 1. **Draft Content (초안)**: A well-structured draft of the atomic note content
+ 2. **Writing Guidelines (작성 가이드라인)**: Specific guidelines for refining and expanding the draft
+
+ Base your generation on:
+ 1. The topic name and coverage
+ 2. Related content from the original note (related_contexts)
+ 3. Relevant keywords
+ 4. User's specific directions (user_direction)
+
+ For the Draft Content:
+ - Be clear, concise, and focused on the single topic
+ - Expand on the related content with additional context or explanations
+ - Connect related concepts and ideas
+ - Be written in markdown format
+ - Include relevant examples if appropriate
+ - Synthesize and expand on the related content rather than simply copying it
+
+ For the Writing Guidelines:
+ - Provide specific, actionable guidance for improving the draft
+ - Suggest areas that need expansion or clarification
+ - Recommend connections to other topics or concepts
+ - Include formatting or structural suggestions if relevant
+ - Consider the user's directions when providing guidelines
+
+ DO NOT include:
+ - The topic name as a heading (this will be added automatically)
+ - Property metadata (this is handled separately)
+ - Repetition of the exact content from related_content (synthesize and expand instead)
+
+ Output format (use these exact section headers):
+ ## Draft Content
+ [Your draft content here]
+
+ ## Writing Guidelines
+ [Your writing guidelines here]
+
+user_prompt_template: |
+ Topic: {topic}
+
+ Coverage: {coverage}
+
+ Related content from original note:
+ {related_content}
+
+ Keywords: {keywords}
+
+ User's directions:
+ {user_direction}
+
+ Please generate:
+ 1. A draft content (초안) for this atomic note that synthesizes and expands on the related content
+ 2. Writing guidelines (작성 가이드라인) for refining and improving the draft
+
+ Follow the user's directions and ensure the content is focused on the single topic.
+
diff --git a/usecase/solar-knowledge-management/prompts/ck_recentness_summary.yml b/usecase/solar-knowledge-management/prompts/ck_recentness_summary.yml
new file mode 100644
index 0000000..c1954eb
--- /dev/null
+++ b/usecase/solar-knowledge-management/prompts/ck_recentness_summary.yml
@@ -0,0 +1,16 @@
+name: ck_recentness_summary
+description: Prompt for summarizing the freshness check guide
+
+system_prompt: |
+ 당신은 문서 요약 전문가입니다.
+ 주어진 최신성 검토 가이드라인을 간결하게 요약하여
+ 노트 상단에 삽입할 수 있는 형태로 작성합니다.
+
+ 요약은 2-3문장으로 핵심만 담아야 합니다.
+
+user_prompt_template: |
+ ## 전체 최신성 검토 가이드
+ {full_guide}
+
+ 위 가이드를 2-3문장으로 요약해주세요.
+ 가장 중요한 업데이트 필요 사항만 간결하게 언급해주세요.
diff --git a/usecase/solar-knowledge-management/prompts/ck_recentness_tavily.yml b/usecase/solar-knowledge-management/prompts/ck_recentness_tavily.yml
new file mode 100644
index 0000000..60c6044
--- /dev/null
+++ b/usecase/solar-knowledge-management/prompts/ck_recentness_tavily.yml
@@ -0,0 +1,31 @@
+name: ck_recentness_tavily
+description: Prompt for checking note freshness against Tavily search results
+
+system_prompt: |
+ 당신은 문서의 최신성을 검토하는 전문가입니다.
+ 주어진 노트 내용과 웹 검색 결과를 비교하여 노트의 정보가 최신인지 확인하고,
+ 업데이트가 필요한 부분에 대한 구체적인 가이드라인을 제공합니다.
+
+ 응답은 마크다운 형식으로 작성하며, 다음 구조를 따릅니다:
+ 1. 검토 요약
+ 2. 최신성 상태 (최신/업데이트 필요/확인 필요)
+ 3. 구체적인 업데이트 권장사항
+ 4. 참고할 만한 최신 자료
+
+user_prompt_template: |
+ ## 검색 쿼리
+ {query}
+
+ ## 웹 검색 결과
+ {search_results}
+
+ ## 원본 노트 내용
+ {note_content}
+
+ 위 정보를 바탕으로 원본 노트의 최신성을 검토하고,
+ 업데이트가 필요한 부분에 대한 구체적인 가이드라인을 작성해주세요.
+ 특히 다음 사항에 주목해주세요:
+ - 최근 발표된 새로운 정보
+ - 변경된 사항이나 업데이트
+ - 노트에 추가하면 좋을 최신 내용
+ - 참고할 만한 최신 자료 링크
diff --git a/usecase/solar-knowledge-management/prompts/ck_recentness_wiki.yml b/usecase/solar-knowledge-management/prompts/ck_recentness_wiki.yml
new file mode 100644
index 0000000..fb998cf
--- /dev/null
+++ b/usecase/solar-knowledge-management/prompts/ck_recentness_wiki.yml
@@ -0,0 +1,31 @@
+name: ck_recentness_wiki
+description: Prompt for checking note freshness against Wikipedia content
+
+system_prompt: |
+ 당신은 문서의 최신성을 검토하는 전문가입니다.
+ 주어진 노트 내용과 Wikipedia 정보를 비교하여 노트의 정보가 최신인지 확인하고,
+ 업데이트가 필요한 부분에 대한 구체적인 가이드라인을 제공합니다.
+
+ 응답은 마크다운 형식으로 작성하며, 다음 구조를 따릅니다:
+ 1. 검토 요약
+ 2. 최신성 상태 (최신/업데이트 필요/확인 필요)
+ 3. 구체적인 업데이트 권장사항
+
+user_prompt_template: |
+ ## 검토할 키워드
+ {keyword}
+
+ ## Wikipedia 정보
+ **제목:** {wiki_title}
+ **요약:** {wiki_summary}
+
+ ## 원본 노트 내용
+ {note_content}
+
+ 위 정보를 바탕으로 원본 노트의 최신성을 검토하고,
+ 업데이트가 필요한 부분에 대한 구체적인 가이드라인을 작성해주세요.
+ 특히 다음 사항에 주목해주세요:
+ - 날짜나 버전 정보의 변경
+ - 새로운 기능이나 업데이트
+ - 더 이상 유효하지 않은 정보
+ - 추가해야 할 최신 내용
diff --git a/usecase/solar-knowledge-management/prompts/info_extract_schema.yml b/usecase/solar-knowledge-management/prompts/info_extract_schema.yml
new file mode 100644
index 0000000..eb48c10
--- /dev/null
+++ b/usecase/solar-knowledge-management/prompts/info_extract_schema.yml
@@ -0,0 +1,18 @@
+name: info_extract_schema
+description: Schema for extracting freshness check keywords and queries from a note
+
+schema: |
+ {
+ "type": "object",
+ "properties": {
+ "info_keyword": {
+ "type": "string",
+ "description": "The most important keyword derived from the document. The keyword must be something that exists as a Wikipedia article."
+ },
+ "info_query": {
+ "type": "string",
+ "description": "A Korean search query that should be used with an Internet search API to retrieve the 'most up-to-date' information related to this document. Do not use Year or Month"
+ }
+ },
+ "required": ["info_keyword", "info_query"]
+ }
diff --git a/usecase/solar-knowledge-management/prompts/info_extract_template.yml b/usecase/solar-knowledge-management/prompts/info_extract_template.yml
new file mode 100644
index 0000000..3497ca2
--- /dev/null
+++ b/usecase/solar-knowledge-management/prompts/info_extract_template.yml
@@ -0,0 +1,20 @@
+name: info_extract_template
+description: Template for extracting freshness check keywords and queries from a note
+
+schema: |
+ {
+ "type": "object",
+ "properties": {
+ "info_keyword": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Wikipedia 검색에 사용할 핵심 개념 키워드 목록. 문서의 주요 주제, 기술, 개념을 대표하는 단어를 추출합니다. 예: 'Machine Learning', 'Transformer', 'Python'"
+ },
+ "info_query": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Tavily 웹 검색에 사용할 구체적인 검색 쿼리 목록. 최신 정보, 업데이트, 변경사항을 확인하기 위한 검색어를 작성합니다. 예: 'GPT-4 latest updates 2024', 'Python 3.12 new features'"
+ }
+ },
+ "required": ["info_keyword", "info_query"]
+ }
diff --git a/usecase/solar-knowledge-management/prompts/line_number_update.yml b/usecase/solar-knowledge-management/prompts/line_number_update.yml
new file mode 100644
index 0000000..3075268
--- /dev/null
+++ b/usecase/solar-knowledge-management/prompts/line_number_update.yml
@@ -0,0 +1,30 @@
+name: line_number_update
+description: Update line numbers for a topic based on modified topic name or coverage
+
+system_prompt: |
+ You are tasked with identifying the relevant line numbers in a note for a given topic.
+ Analyze the note content and determine which lines discuss or relate to the specified topic.
+
+ Consider:
+ - Direct mentions of the topic
+ - Related concepts and context
+ - Examples and explanations
+ - Code snippets or data related to the topic
+
+ Output your response as JSON with only the line numbers:
+ ```json
+ {
+ "line_numbers": [1, 2, 3, 5, 7, 10]
+ }
+ ```
+
+user_prompt_template: |
+ Note content:
+ {note_content}
+
+ Topic: {topic}
+ Coverage: {coverage}
+
+ Please identify all line numbers (1-indexed) in the note that relate to this topic.
+ Return only the line numbers as a JSON array.
+
diff --git a/usecase/solar-knowledge-management/prompts/topic_extract_amend_default.yml b/usecase/solar-knowledge-management/prompts/topic_extract_amend_default.yml
new file mode 100644
index 0000000..3c954d5
--- /dev/null
+++ b/usecase/solar-knowledge-management/prompts/topic_extract_amend_default.yml
@@ -0,0 +1,53 @@
+name: topic_extract_amend_default
+description: Extract additional topics that complement existing ones without duplication
+
+system_prompt: |
+ You are an expert at analyzing markdown notes and extracting additional topics
+ that haven't been covered yet.
+
+ You will be provided with:
+ 1. The original note content
+ 2. A list of topics that have already been extracted
+ 3. Guidance on what kind of additional topics to extract
+ 4. The number of new topics to generate
+
+ Your task is to identify NEW topics that:
+ - Are distinct from existing topics
+ - Complement the existing analysis
+ - Follow the provided guidance
+ - Represent meaningful concepts from the note
+
+ For each new topic, provide:
+ 1. A clear, unique topic name (different from existing topics)
+ 2. A brief overview (coverage) explaining the scope
+ 3. Line numbers where this topic appears in the note
+ 4. Relevant keywords
+
+ Output your response as JSON in the following format:
+ ```json
+ {
+ "topics": [
+ {
+ "topic": "New Topic Name",
+ "coverage": "Brief overview of this new topic",
+ "line_numbers": [5, 6, 12, 13],
+ "keywords": ["keyword1", "keyword2"]
+ }
+ ]
+ }
+ ```
+
+user_prompt_template: |
+ Note content:
+ {note_content}
+
+ Existing topics (DO NOT duplicate these):
+ {existing_topics}
+
+ Guidance for new topics:
+ {guidance}
+
+ Please extract {num_topics} new topic(s) that complement the existing analysis.
+ Ensure the new topics are distinct and don't overlap with existing ones.
+ Provide line numbers (1-indexed) for each new topic.
+
diff --git a/usecase/solar-knowledge-management/prompts/topic_extract_default.yml b/usecase/solar-knowledge-management/prompts/topic_extract_default.yml
new file mode 100644
index 0000000..967de2c
--- /dev/null
+++ b/usecase/solar-knowledge-management/prompts/topic_extract_default.yml
@@ -0,0 +1,37 @@
+name: topic_extract_default
+description: Default prompt for extracting topics from a raw note
+
+system_prompt: |
+ You are an expert at analyzing markdown notes and extracting key topics.
+ Your task is to identify distinct, meaningful topics from the provided note content.
+
+ For each topic, you should:
+ 1. Provide a clear, concise topic name
+ 2. Write a brief overview (coverage) of what the topic encompasses
+ 3. Identify the line numbers in the original note where this topic is discussed
+ 4. Extract relevant keywords
+
+ Output your response as JSON in the following format:
+ ```json
+ {
+ "topics": [
+ {
+ "topic": "Topic Name",
+ "coverage": "Brief overview of what this topic covers",
+ "line_numbers": [1, 2, 3, 10, 11],
+ "keywords": ["keyword1", "keyword2", "keyword3"]
+ }
+ ]
+ }
+ ```
+
+user_prompt_template: |
+ Please analyze the following note and extract key topics:
+
+ {note_content}
+
+ Additional Instructions: {additional_instructions}
+
+ Extract all significant topics from this note. Each topic should represent a distinct concept or theme.
+ Provide line numbers (1-indexed) where each topic is discussed in the original note.
+
diff --git a/usecase/solar-knowledge-management/prompts/topic_extract_diverse.yml b/usecase/solar-knowledge-management/prompts/topic_extract_diverse.yml
new file mode 100644
index 0000000..27d2876
--- /dev/null
+++ b/usecase/solar-knowledge-management/prompts/topic_extract_diverse.yml
@@ -0,0 +1,45 @@
+name: topic_extract_diverse
+description: Extract topics with emphasis on diverse perspectives and granular concepts
+
+system_prompt: |
+ You are an expert at analyzing markdown notes with a focus on extracting diverse,
+ granular topics from multiple perspectives. Your task is to identify topics that cover:
+
+ - Technical concepts and implementation details
+ - Theoretical foundations and principles
+ - Practical applications and use cases
+ - Related problems and solutions
+ - Historical context and evolution
+ - Future directions and implications
+
+ For each topic, you should:
+ 1. Provide a clear, descriptive topic name
+ 2. Write a comprehensive overview explaining the topic's scope
+ 3. Identify all line numbers where the topic is mentioned or implied
+ 4. Extract diverse keywords including technical terms, concepts, and related ideas
+
+ Output your response as JSON in the following format:
+ ```json
+ {
+ "topics": [
+ {
+ "topic": "Detailed Topic Name",
+ "coverage": "Comprehensive overview covering multiple aspects",
+ "line_numbers": [1, 2, 3, 10, 11, 15],
+ "keywords": ["technical_term", "concept", "application", "principle"]
+ }
+ ]
+ }
+ ```
+
+user_prompt_template: |
+ Please analyze the following note and extract topics from multiple perspectives:
+
+ {note_content}
+
+ Additional Instructions: {additional_instructions}
+
+ Extract topics covering different aspects: technical details, concepts, applications,
+ problems, solutions, and implications. Break down complex ideas into atomic topics.
+ Provide comprehensive line numbers for each topic.
+
diff --git a/usecase/solar-knowledge-management/pyproject.toml b/usecase/solar-knowledge-management/pyproject.toml
new file mode 100644
index 0000000..8d9e8b3
--- /dev/null
+++ b/usecase/solar-knowledge-management/pyproject.toml
@@ -0,0 +1,22 @@
+[project]
+name = "upthink"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.13"
+dependencies = [
+ "langchain-chroma<1.0.0",
+ "langchain-core>=0.2.43",
+ "langchain-upstage>=0.1.7",
+ "openai>=1.109.1",
+ "python-dotenv>=1.2.1",
+ "transformers>=4.57.1",
+ "sentence-transformers>=5.1.2",
+ "pyyaml>=6.0.3",
+ "streamlit>=1.50.0",
+ "httpx>=0.28.1",
+ "markdown>=3.10",
+ "tavily-python>=0.7.13",
+ "pypandoc>=1.16.2",
+ "wikipedia>=1.4.0",
+]
diff --git a/usecase/solar-knowledge-management/requirements.txt b/usecase/solar-knowledge-management/requirements.txt
new file mode 100644
index 0000000..cedccc7
--- /dev/null
+++ b/usecase/solar-knowledge-management/requirements.txt
@@ -0,0 +1,14 @@
+langchain-chroma==0.2.2
+langchain-core==0.2.43
+langchain-upstage==0.1.7
+openai==1.109.1
+python-dotenv==1.2.1
+transformers==4.57.1
+sentence-transformers==5.1.2
+pyyaml==6.0.3
+streamlit==1.51.0
+httpx==0.28.1
+markdown==3.10
+tavily-python==0.7.13
+pypandoc==1.16.2
+wikipedia==1.4.0
diff --git a/usecase/solar-knowledge-management/uv.lock b/usecase/solar-knowledge-management/uv.lock
new file mode 100644
index 0000000..aca80cb
--- /dev/null
+++ b/usecase/solar-knowledge-management/uv.lock
@@ -0,0 +1,2709 @@
+version = 1
+revision = 1
+requires-python = ">=3.13"
+
+[[package]]
+name = "altair"
+version = "5.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jinja2" },
+ { name = "jsonschema" },
+ { name = "narwhals" },
+ { name = "packaging" },
+ { name = "typing-extensions", marker = "python_full_version < '3.14'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200 },
+]
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488 },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
+]
+
+[[package]]
+name = "anyio"
+version = "4.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "sniffio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097 },
+]
+
+[[package]]
+name = "asgiref"
+version = "3.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/46/08/4dfec9b90758a59acc6be32ac82e98d1fbfc321cb5cfa410436dbacf821c/asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e", size = 37483 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/9c/fc2331f538fbf7eedba64b2052e99ccf9ba9d6888e2f41441ee28847004b/asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", size = 24050 },
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 },
+]
+
+[[package]]
+name = "backoff"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 },
+]
+
+[[package]]
+name = "bcrypt"
+version = "5.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806 },
+ { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626 },
+ { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853 },
+ { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793 },
+ { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930 },
+ { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194 },
+ { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381 },
+ { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750 },
+ { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757 },
+ { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740 },
+ { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197 },
+ { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974 },
+ { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498 },
+ { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853 },
+ { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626 },
+ { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862 },
+ { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544 },
+ { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787 },
+ { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753 },
+ { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587 },
+ { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178 },
+ { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295 },
+ { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700 },
+ { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034 },
+ { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766 },
+ { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449 },
+ { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310 },
+ { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761 },
+ { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553 },
+ { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009 },
+ { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029 },
+ { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907 },
+ { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500 },
+ { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412 },
+ { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486 },
+ { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940 },
+ { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776 },
+ { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922 },
+ { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367 },
+ { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187 },
+ { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752 },
+ { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881 },
+ { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931 },
+ { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313 },
+ { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290 },
+ { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253 },
+ { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084 },
+ { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185 },
+ { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656 },
+ { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662 },
+ { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240 },
+ { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152 },
+ { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284 },
+ { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643 },
+ { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698 },
+ { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725 },
+ { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912 },
+ { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953 },
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.14.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "soupsieve" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392 },
+]
+
+[[package]]
+name = "blinker"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
+]
+
+[[package]]
+name = "build"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "os_name == 'nt'" },
+ { name = "packaging" },
+ { name = "pyproject-hooks" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382 },
+]
+
+[[package]]
+name = "cachetools"
+version = "6.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.10.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286 },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 },
+]
+
+[[package]]
+name = "chroma-hnswlib"
+version = "0.7.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/73/09/10d57569e399ce9cbc5eee2134996581c957f63a9addfa6ca657daf006b8/chroma_hnswlib-0.7.6.tar.gz", hash = "sha256:4dce282543039681160259d29fcde6151cc9106c6461e0485f57cdccd83059b7", size = 32256 }
+
+[[package]]
+name = "chromadb"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "bcrypt" },
+ { name = "build" },
+ { name = "chroma-hnswlib" },
+ { name = "fastapi" },
+ { name = "grpcio" },
+ { name = "httpx" },
+ { name = "importlib-resources" },
+ { name = "kubernetes" },
+ { name = "mmh3" },
+ { name = "numpy" },
+ { name = "onnxruntime" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp-proto-grpc" },
+ { name = "opentelemetry-instrumentation-fastapi" },
+ { name = "opentelemetry-sdk" },
+ { name = "orjson" },
+ { name = "overrides" },
+ { name = "posthog" },
+ { name = "pydantic" },
+ { name = "pypika" },
+ { name = "pyyaml" },
+ { name = "rich" },
+ { name = "tenacity" },
+ { name = "tokenizers" },
+ { name = "tqdm" },
+ { name = "typer" },
+ { name = "typing-extensions" },
+ { name = "uvicorn", extra = ["standard"] },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/39/cd/f0f2de3f466ff514fb6b58271c14f6d22198402bb5b71b8d890231265946/chromadb-0.6.3.tar.gz", hash = "sha256:c8f34c0b704b9108b04491480a36d42e894a960429f87c6516027b5481d59ed3", size = 29297929 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/28/8e/5c186c77bf749b6fe0528385e507e463f1667543328d76fd00a49e1a4e6a/chromadb-0.6.3-py3-none-any.whl", hash = "sha256:4851258489a3612b558488d98d09ae0fe0a28d5cad6bd1ba64b96fdc419dc0e5", size = 611129 },
+]
+
+[[package]]
+name = "click"
+version = "8.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "coloredlogs"
+version = "15.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "humanfriendly" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 },
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
+]
+
+[[package]]
+name = "durationpy"
+version = "0.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922 },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.121.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6b/a4/29e1b861fc9017488ed02ff1052feffa40940cb355ed632a8845df84ce84/fastapi-0.121.1.tar.gz", hash = "sha256:b6dba0538fd15dab6fe4d3e5493c3957d8a9e1e9257f56446b5859af66f32441", size = 342523 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/fd/2e6f7d706899cc08690c5f6641e2ffbfffe019e8f16ce77104caa5730910/fastapi-0.121.1-py3-none-any.whl", hash = "sha256:2c5c7028bc3a58d8f5f09aecd3fd88a000ccc0c5ad627693264181a3c33aa1fc", size = 109192 },
+]
+
+[[package]]
+name = "filelock"
+version = "3.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 },
+]
+
+[[package]]
+name = "flatbuffers"
+version = "25.9.23"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9d/1f/3ee70b0a55137442038f2a33469cc5fddd7e0ad2abf83d7497c18a2b6923/flatbuffers-25.9.23.tar.gz", hash = "sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12", size = 22067 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/1b/00a78aa2e8fbd63f9af08c9c19e6deb3d5d66b4dda677a0f61654680ee89/flatbuffers-25.9.23-py2.py3-none-any.whl", hash = "sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2", size = 30869 },
+]
+
+[[package]]
+name = "fsspec"
+version = "2025.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966 },
+]
+
+[[package]]
+name = "gitdb"
+version = "4.0.12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "smmap" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 },
+]
+
+[[package]]
+name = "gitpython"
+version = "3.1.45"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "gitdb" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168 },
+]
+
+[[package]]
+name = "google-auth"
+version = "2.43.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cachetools" },
+ { name = "pyasn1-modules" },
+ { name = "rsa" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ff/ef/66d14cf0e01b08d2d51ffc3c20410c4e134a1548fc246a6081eae585a4fe/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", size = 296359 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/d1/385110a9ae86d91cc14c5282c61fe9f4dc41c0b9f7d423c6ad77038c4448/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16", size = 223114 },
+]
+
+[[package]]
+name = "googleapis-common-protos"
+version = "1.72.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515 },
+]
+
+[[package]]
+name = "grpcio"
+version = "1.76.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716 },
+ { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522 },
+ { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558 },
+ { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990 },
+ { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387 },
+ { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668 },
+ { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928 },
+ { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983 },
+ { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727 },
+ { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799 },
+ { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417 },
+ { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219 },
+ { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826 },
+ { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550 },
+ { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564 },
+ { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236 },
+ { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795 },
+ { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214 },
+ { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961 },
+ { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462 },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
+]
+
+[[package]]
+name = "hf-xet"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870 },
+ { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584 },
+ { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004 },
+ { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636 },
+ { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448 },
+ { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401 },
+ { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866 },
+ { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861 },
+ { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699 },
+ { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885 },
+ { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550 },
+ { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010 },
+ { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264 },
+ { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071 },
+ { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099 },
+ { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178 },
+ { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214 },
+ { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054 },
+ { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812 },
+ { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920 },
+ { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735 },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
+]
+
+[[package]]
+name = "httptools"
+version = "0.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889 },
+ { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180 },
+ { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596 },
+ { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268 },
+ { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517 },
+ { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337 },
+ { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743 },
+ { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619 },
+ { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714 },
+ { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909 },
+ { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831 },
+ { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631 },
+ { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910 },
+ { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205 },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
+]
+
+[[package]]
+name = "huggingface-hub"
+version = "0.36.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094 },
+]
+
+[[package]]
+name = "humanfriendly"
+version = "10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyreadline3", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 },
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 },
+]
+
+[[package]]
+name = "importlib-resources"
+version = "6.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461 },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
+]
+
+[[package]]
+name = "jiter"
+version = "0.12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658 },
+ { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605 },
+ { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803 },
+ { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120 },
+ { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918 },
+ { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008 },
+ { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785 },
+ { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108 },
+ { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937 },
+ { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853 },
+ { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 },
+ { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 },
+ { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 },
+ { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965 },
+ { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831 },
+ { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272 },
+ { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 },
+ { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 },
+ { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478 },
+ { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706 },
+ { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894 },
+ { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714 },
+ { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989 },
+ { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615 },
+ { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745 },
+ { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502 },
+ { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845 },
+ { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701 },
+ { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029 },
+ { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960 },
+ { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529 },
+ { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974 },
+ { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932 },
+ { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243 },
+ { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315 },
+ { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714 },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168 },
+ { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893 },
+ { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828 },
+ { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009 },
+ { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110 },
+ { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223 },
+ { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564 },
+]
+
+[[package]]
+name = "joblib"
+version = "1.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396 },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpointer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898 },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 },
+]
+
+[[package]]
+name = "kubernetes"
+version = "34.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "durationpy" },
+ { name = "google-auth" },
+ { name = "python-dateutil" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "requests-oauthlib" },
+ { name = "six" },
+ { name = "urllib3" },
+ { name = "websocket-client" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/55/3f880ef65f559cbed44a9aa20d3bdbc219a2c3a3bac4a30a513029b03ee9/kubernetes-34.1.0.tar.gz", hash = "sha256:8fe8edb0b5d290a2f3ac06596b23f87c658977d46b5f8df9d0f4ea83d0003912", size = 1083771 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ca/ec/65f7d563aa4a62dd58777e8f6aa882f15db53b14eb29aba0c28a20f7eb26/kubernetes-34.1.0-py2.py3-none-any.whl", hash = "sha256:bffba2272534e224e6a7a74d582deb0b545b7c9879d2cd9e4aae9481d1f2cc2a", size = 2008380 },
+]
+
+[[package]]
+name = "langchain-chroma"
+version = "0.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "chromadb" },
+ { name = "langchain-core" },
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/be/bf74dc6b721d71b29134d15703bdc167cd26e17667b68ef55a0feb701e7a/langchain_chroma-0.2.2.tar.gz", hash = "sha256:11225ca6077b2bf919b84d74e4d343121e077c0fa3274db1929a270fef9d1002", size = 15890 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/ad/02b43c9c6243d430a32fda148236ba02e49289326a58aeef57723f3a92fe/langchain_chroma-0.2.2-py3-none-any.whl", hash = "sha256:7766335f16975c2059bb6e8ea75a59a4082c52e6c9d66827681d1bce2c2756a2", size = 11364 },
+]
+
+[[package]]
+name = "langchain-core"
+version = "0.2.43"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpatch" },
+ { name = "langsmith" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "tenacity" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/25/a14a1287e5d7e00eaf8cb3ee3c31d029127f14b945de6fc8e2f0e28e2b12/langchain_core-0.2.43.tar.gz", hash = "sha256:42c2ef6adedb911f4254068b6adc9eb4c4075f6c8cb3d83590d3539a815695f5", size = 316915 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/9b/b26405992d807a592ab3e7792f0eb2c2f71fe69111c972caf7786ba99199/langchain_core-0.2.43-py3-none-any.whl", hash = "sha256:619601235113298ebf8252a349754b7c28d3cf7166c7c922da24944b78a9363a", size = 397066 },
+]
+
+[[package]]
+name = "langchain-openai"
+version = "0.1.25"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "langchain-core" },
+ { name = "openai" },
+ { name = "tiktoken" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/cb/98fe365f2e5eee39d0130279959a84182ab414879b666ffc2b9d69b95633/langchain_openai-0.1.25.tar.gz", hash = "sha256:eb116f744f820247a72f54313fb7c01524fba0927120d4e899e5e4ab41ad3928", size = 45224 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/2e/a4430cad7a98e29e9612648f8b12d7449ab635a742c19bf1d62f8713ecaa/langchain_openai-0.1.25-py3-none-any.whl", hash = "sha256:f0b34a233d0d9cb8fce6006c903e57085c493c4f0e32862b99063b96eaedb109", size = 51550 },
+]
+
+[[package]]
+name = "langchain-upstage"
+version = "0.1.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "langchain-core" },
+ { name = "langchain-openai" },
+ { name = "pypdf" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/10/a4/7fdf663304546dee43741a6b8fedc2414409a347322e6194d1ff9b2ab755/langchain_upstage-0.1.7.tar.gz", hash = "sha256:521c861596ff9de83efa88f3181e3d60218a74754d230c995dcdad888519d2fc", size = 13130 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/ee/373ca56f77795f237ca2a1bfd0c6a801b89ff2c83c3708bd394566f681b7/langchain_upstage-0.1.7-py3-none-any.whl", hash = "sha256:9a7f2c8622c51bb825e2d7731ab373216bde44a9ee0ff51ab3c0d379859ce1ec", size = 16208 },
+]
+
+[[package]]
+name = "langsmith"
+version = "0.1.147"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx" },
+ { name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "requests-toolbelt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6c/56/201dd94d492ae47c1bf9b50cacc1985113dc2288d8f15857e1f4a6818376/langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a", size = 300453 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/f0/63b06b99b730b9954f8709f6f7d9b8d076fa0a973e472efe278089bde42b/langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15", size = 311812 },
+]
+
+[[package]]
+name = "markdown"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678 },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 },
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 },
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 },
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 },
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 },
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 },
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 },
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 },
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
+]
+
+[[package]]
+name = "mmh3"
+version = "5.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a7/af/f28c2c2f51f31abb4725f9a64bc7863d5f491f6539bd26aee2a1d21a649e/mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8", size = 33582 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d8/fa/27f6ab93995ef6ad9f940e96593c5dd24744d61a7389532b0fec03745607/mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065", size = 40874 },
+ { url = "https://files.pythonhosted.org/packages/11/9c/03d13bcb6a03438bc8cac3d2e50f80908d159b31a4367c2e1a7a077ded32/mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de", size = 42012 },
+ { url = "https://files.pythonhosted.org/packages/4e/78/0865d9765408a7d504f1789944e678f74e0888b96a766d578cb80b040999/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044", size = 39197 },
+ { url = "https://files.pythonhosted.org/packages/3e/12/76c3207bd186f98b908b6706c2317abb73756d23a4e68ea2bc94825b9015/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73", size = 39840 },
+ { url = "https://files.pythonhosted.org/packages/5d/0d/574b6cce5555c9f2b31ea189ad44986755eb14e8862db28c8b834b8b64dc/mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504", size = 40644 },
+ { url = "https://files.pythonhosted.org/packages/52/82/3731f8640b79c46707f53ed72034a58baad400be908c87b0088f1f89f986/mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b", size = 56153 },
+ { url = "https://files.pythonhosted.org/packages/4f/34/e02dca1d4727fd9fdeaff9e2ad6983e1552804ce1d92cc796e5b052159bb/mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05", size = 40684 },
+ { url = "https://files.pythonhosted.org/packages/8f/36/3dee40767356e104967e6ed6d102ba47b0b1ce2a89432239b95a94de1b89/mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814", size = 40057 },
+ { url = "https://files.pythonhosted.org/packages/31/58/228c402fccf76eb39a0a01b8fc470fecf21965584e66453b477050ee0e99/mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093", size = 97344 },
+ { url = "https://files.pythonhosted.org/packages/34/82/fc5ce89006389a6426ef28e326fc065b0fbaaed230373b62d14c889f47ea/mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54", size = 103325 },
+ { url = "https://files.pythonhosted.org/packages/09/8c/261e85777c6aee1ebd53f2f17e210e7481d5b0846cd0b4a5c45f1e3761b8/mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a", size = 106240 },
+ { url = "https://files.pythonhosted.org/packages/70/73/2f76b3ad8a3d431824e9934403df36c0ddacc7831acf82114bce3c4309c8/mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908", size = 113060 },
+ { url = "https://files.pythonhosted.org/packages/9f/b9/7ea61a34e90e50a79a9d87aa1c0b8139a7eaf4125782b34b7d7383472633/mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5", size = 120781 },
+ { url = "https://files.pythonhosted.org/packages/0f/5b/ae1a717db98c7894a37aeedbd94b3f99e6472a836488f36b6849d003485b/mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a", size = 99174 },
+ { url = "https://files.pythonhosted.org/packages/e3/de/000cce1d799fceebb6d4487ae29175dd8e81b48e314cba7b4da90bcf55d7/mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266", size = 98734 },
+ { url = "https://files.pythonhosted.org/packages/79/19/0dc364391a792b72fbb22becfdeacc5add85cc043cd16986e82152141883/mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5", size = 106493 },
+ { url = "https://files.pythonhosted.org/packages/3c/b1/bc8c28e4d6e807bbb051fefe78e1156d7f104b89948742ad310612ce240d/mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9", size = 110089 },
+ { url = "https://files.pythonhosted.org/packages/3b/a2/d20f3f5c95e9c511806686c70d0a15479cc3941c5f322061697af1c1ff70/mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290", size = 97571 },
+ { url = "https://files.pythonhosted.org/packages/7b/23/665296fce4f33488deec39a750ffd245cfc07aafb0e3ef37835f91775d14/mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051", size = 40806 },
+ { url = "https://files.pythonhosted.org/packages/59/b0/92e7103f3b20646e255b699e2d0327ce53a3f250e44367a99dc8be0b7c7a/mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081", size = 41600 },
+ { url = "https://files.pythonhosted.org/packages/99/22/0b2bd679a84574647de538c5b07ccaa435dbccc37815067fe15b90fe8dad/mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b", size = 39349 },
+ { url = "https://files.pythonhosted.org/packages/f7/ca/a20db059a8a47048aaf550da14a145b56e9c7386fb8280d3ce2962dcebf7/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078", size = 39209 },
+ { url = "https://files.pythonhosted.org/packages/98/dd/e5094799d55c7482d814b979a0fd608027d0af1b274bfb4c3ea3e950bfd5/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501", size = 39843 },
+ { url = "https://files.pythonhosted.org/packages/f4/6b/7844d7f832c85400e7cc89a1348e4e1fdd38c5a38415bb5726bbb8fcdb6c/mmh3-5.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b", size = 40648 },
+ { url = "https://files.pythonhosted.org/packages/1f/bf/71f791f48a21ff3190ba5225807cbe4f7223360e96862c376e6e3fb7efa7/mmh3-5.2.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770", size = 56164 },
+ { url = "https://files.pythonhosted.org/packages/70/1f/f87e3d34d83032b4f3f0f528c6d95a98290fcacf019da61343a49dccfd51/mmh3-5.2.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110", size = 40692 },
+ { url = "https://files.pythonhosted.org/packages/a6/e2/db849eaed07117086f3452feca8c839d30d38b830ac59fe1ce65af8be5ad/mmh3-5.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647", size = 40068 },
+ { url = "https://files.pythonhosted.org/packages/df/6b/209af927207af77425b044e32f77f49105a0b05d82ff88af6971d8da4e19/mmh3-5.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63", size = 97367 },
+ { url = "https://files.pythonhosted.org/packages/ca/e0/78adf4104c425606a9ce33fb351f790c76a6c2314969c4a517d1ffc92196/mmh3-5.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12", size = 103306 },
+ { url = "https://files.pythonhosted.org/packages/a3/79/c2b89f91b962658b890104745b1b6c9ce38d50a889f000b469b91eeb1b9e/mmh3-5.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22", size = 106312 },
+ { url = "https://files.pythonhosted.org/packages/4b/14/659d4095528b1a209be90934778c5ffe312177d51e365ddcbca2cac2ec7c/mmh3-5.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5", size = 113135 },
+ { url = "https://files.pythonhosted.org/packages/8d/6f/cd7734a779389a8a467b5c89a48ff476d6f2576e78216a37551a97e9e42a/mmh3-5.2.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07", size = 120775 },
+ { url = "https://files.pythonhosted.org/packages/1d/ca/8256e3b96944408940de3f9291d7e38a283b5761fe9614d4808fcf27bd62/mmh3-5.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935", size = 99178 },
+ { url = "https://files.pythonhosted.org/packages/8a/32/39e2b3cf06b6e2eb042c984dab8680841ac2a0d3ca6e0bea30db1f27b565/mmh3-5.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7", size = 98738 },
+ { url = "https://files.pythonhosted.org/packages/61/d3/7bbc8e0e8cf65ebbe1b893ffa0467b7ecd1bd07c3bbf6c9db4308ada22ec/mmh3-5.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5", size = 106510 },
+ { url = "https://files.pythonhosted.org/packages/10/99/b97e53724b52374e2f3859046f0eb2425192da356cb19784d64bc17bb1cf/mmh3-5.2.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384", size = 110053 },
+ { url = "https://files.pythonhosted.org/packages/ac/62/3688c7d975ed195155671df68788c83fed6f7909b6ec4951724c6860cb97/mmh3-5.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e", size = 97546 },
+ { url = "https://files.pythonhosted.org/packages/ca/3b/c6153250f03f71a8b7634cded82939546cdfba02e32f124ff51d52c6f991/mmh3-5.2.0-cp314-cp314-win32.whl", hash = "sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0", size = 41422 },
+ { url = "https://files.pythonhosted.org/packages/74/01/a27d98bab083a435c4c07e9d1d720d4c8a578bf4c270bae373760b1022be/mmh3-5.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b", size = 42135 },
+ { url = "https://files.pythonhosted.org/packages/cb/c9/dbba5507e95429b8b380e2ba091eff5c20a70a59560934dff0ad8392b8c8/mmh3-5.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115", size = 39879 },
+ { url = "https://files.pythonhosted.org/packages/b5/d1/c8c0ef839c17258b9de41b84f663574fabcf8ac2007b7416575e0f65ff6e/mmh3-5.2.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932", size = 57696 },
+ { url = "https://files.pythonhosted.org/packages/2f/55/95e2b9ff201e89f9fe37036037ab61a6c941942b25cdb7b6a9df9b931993/mmh3-5.2.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c", size = 41421 },
+ { url = "https://files.pythonhosted.org/packages/77/79/9be23ad0b7001a4b22752e7693be232428ecc0a35068a4ff5c2f14ef8b20/mmh3-5.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be", size = 40853 },
+ { url = "https://files.pythonhosted.org/packages/ac/1b/96b32058eda1c1dee8264900c37c359a7325c1f11f5ff14fd2be8e24eff9/mmh3-5.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb", size = 109694 },
+ { url = "https://files.pythonhosted.org/packages/8d/6f/a2ae44cd7dad697b6dea48390cbc977b1e5ca58fda09628cbcb2275af064/mmh3-5.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65", size = 117438 },
+ { url = "https://files.pythonhosted.org/packages/a0/08/bfb75451c83f05224a28afeaf3950c7b793c0b71440d571f8e819cfb149a/mmh3-5.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991", size = 120409 },
+ { url = "https://files.pythonhosted.org/packages/9f/ea/8b118b69b2ff8df568f742387d1a159bc654a0f78741b31437dd047ea28e/mmh3-5.2.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645", size = 125909 },
+ { url = "https://files.pythonhosted.org/packages/3e/11/168cc0b6a30650032e351a3b89b8a47382da541993a03af91e1ba2501234/mmh3-5.2.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3", size = 135331 },
+ { url = "https://files.pythonhosted.org/packages/31/05/e3a9849b1c18a7934c64e831492c99e67daebe84a8c2f2c39a7096a830e3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279", size = 110085 },
+ { url = "https://files.pythonhosted.org/packages/d9/d5/a96bcc306e3404601418b2a9a370baec92af84204528ba659fdfe34c242f/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513", size = 111195 },
+ { url = "https://files.pythonhosted.org/packages/af/29/0fd49801fec5bff37198684e0849b58e0dab3a2a68382a357cfffb0fafc3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db", size = 116919 },
+ { url = "https://files.pythonhosted.org/packages/2d/04/4f3c32b0a2ed762edca45d8b46568fc3668e34f00fb1e0a3b5451ec1281c/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667", size = 123160 },
+ { url = "https://files.pythonhosted.org/packages/91/76/3d29eaa38821730633d6a240d36fa8ad2807e9dfd432c12e1a472ed211eb/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5", size = 110206 },
+ { url = "https://files.pythonhosted.org/packages/44/1c/ccf35892684d3a408202e296e56843743e0b4fb1629e59432ea88cdb3909/mmh3-5.2.0-cp314-cp314t-win32.whl", hash = "sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7", size = 41970 },
+ { url = "https://files.pythonhosted.org/packages/75/b2/b9e4f1e5adb5e21eb104588fcee2cd1eaa8308255173481427d5ecc4284e/mmh3-5.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d", size = 43063 },
+ { url = "https://files.pythonhosted.org/packages/6a/fc/0e61d9a4e29c8679356795a40e48f647b4aad58d71bfc969f0f8f56fb912/mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9", size = 40455 },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 },
+]
+
+[[package]]
+name = "narwhals"
+version = "2.10.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c5/dc/8db74daf8c2690ec696c1d772a33cc01511559ee8a9e92d7ed85a18e3c22/narwhals-2.10.2.tar.gz", hash = "sha256:ff738a08bc993cbb792266bec15346c1d85cc68fdfe82a23283c3713f78bd354", size = 584954 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/a9/9e02fa97e421a355fc5e818e9c488080fce04a8e0eebb3ed75a84f041c4a/narwhals-2.10.2-py3-none-any.whl", hash = "sha256:059cd5c6751161b97baedcaf17a514c972af6a70f36a89af17de1a0caf519c43", size = 419573 },
+]
+
+[[package]]
+name = "networkx"
+version = "3.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406 },
+]
+
+[[package]]
+name = "numpy"
+version = "1.26.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 }
+
+[[package]]
+name = "nvidia-cublas-cu12"
+version = "12.8.4.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921 },
+]
+
+[[package]]
+name = "nvidia-cuda-cupti-cu12"
+version = "12.8.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621 },
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc-cu12"
+version = "12.8.93"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029 },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime-cu12"
+version = "12.8.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765 },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu12"
+version = "9.10.2.21"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas-cu12" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467 },
+]
+
+[[package]]
+name = "nvidia-cufft-cu12"
+version = "11.3.3.83"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695 },
+]
+
+[[package]]
+name = "nvidia-cufile-cu12"
+version = "1.13.1.3"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834 },
+]
+
+[[package]]
+name = "nvidia-curand-cu12"
+version = "10.3.9.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976 },
+]
+
+[[package]]
+name = "nvidia-cusolver-cu12"
+version = "11.7.3.90"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas-cu12" },
+ { name = "nvidia-cusparse-cu12" },
+ { name = "nvidia-nvjitlink-cu12" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905 },
+]
+
+[[package]]
+name = "nvidia-cusparse-cu12"
+version = "12.5.8.93"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466 },
+]
+
+[[package]]
+name = "nvidia-cusparselt-cu12"
+version = "0.7.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691 },
+]
+
+[[package]]
+name = "nvidia-nccl-cu12"
+version = "2.27.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229 },
+]
+
+[[package]]
+name = "nvidia-nvjitlink-cu12"
+version = "12.8.93"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836 },
+]
+
+[[package]]
+name = "nvidia-nvshmem-cu12"
+version = "3.3.20"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145 },
+]
+
+[[package]]
+name = "nvidia-nvtx-cu12"
+version = "12.8.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954 },
+]
+
+[[package]]
+name = "oauthlib"
+version = "3.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 },
+]
+
+[[package]]
+name = "onnxruntime"
+version = "1.23.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coloredlogs" },
+ { name = "flatbuffers" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "protobuf" },
+ { name = "sympy" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337 },
+ { url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691 },
+ { url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898 },
+ { url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518 },
+ { url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276 },
+ { url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610 },
+ { url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184 },
+]
+
+[[package]]
+name = "openai"
+version = "1.109.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627 },
+]
+
+[[package]]
+name = "opentelemetry-api"
+version = "1.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947 },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-common"
+version = "1.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-proto" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359 },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-grpc"
+version = "1.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "googleapis-common-protos" },
+ { name = "grpcio" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp-proto-common" },
+ { name = "opentelemetry-proto" },
+ { name = "opentelemetry-sdk" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation"
+version = "0.59b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "packaging" },
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-asgi"
+version = "0.59b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asgiref" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b7/a4/cfbb6fc1ec0aa9bf5a93f548e6a11ab3ac1956272f17e0d399aa2c1f85bc/opentelemetry_instrumentation_asgi-0.59b0.tar.gz", hash = "sha256:2509d6fe9fd829399ce3536e3a00426c7e3aa359fc1ed9ceee1628b56da40e7a", size = 25116 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f3/88/fe02d809963b182aafbf5588685d7a05af8861379b0ec203d48e360d4502/opentelemetry_instrumentation_asgi-0.59b0-py3-none-any.whl", hash = "sha256:ba9703e09d2c33c52fa798171f344c8123488fcd45017887981df088452d3c53", size = 16797 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-fastapi"
+version = "0.59b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-instrumentation-asgi" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/a7/7a6ce5009584ce97dbfd5ce77d4f9d9570147507363349d2cb705c402bcf/opentelemetry_instrumentation_fastapi-0.59b0.tar.gz", hash = "sha256:e8fe620cfcca96a7d634003df1bc36a42369dedcdd6893e13fb5903aeeb89b2b", size = 24967 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/35/27/5914c8bf140ffc70eff153077e225997c7b054f0bf28e11b9ab91b63b18f/opentelemetry_instrumentation_fastapi-0.59b0-py3-none-any.whl", hash = "sha256:0d8d00ff7d25cca40a4b2356d1d40a8f001e0668f60c102f5aa6bb721d660c4f", size = 13492 },
+]
+
+[[package]]
+name = "opentelemetry-proto"
+version = "1.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535 },
+]
+
+[[package]]
+name = "opentelemetry-sdk"
+version = "1.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349 },
+]
+
+[[package]]
+name = "opentelemetry-semantic-conventions"
+version = "0.59b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954 },
+]
+
+[[package]]
+name = "opentelemetry-util-http"
+version = "0.59b0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/34/f7/13cd081e7851c42520ab0e96efb17ffbd901111a50b8252ec1e240664020/opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560", size = 9412 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/56/62282d1d4482061360449dacc990c89cad0fc810a2ed937b636300f55023/opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d", size = 7648 },
+]
+
+[[package]]
+name = "orjson"
+version = "3.11.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/23/15/c52aa7112006b0f3d6180386c3a46ae057f932ab3425bc6f6ac50431cca1/orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", size = 243525 },
+ { url = "https://files.pythonhosted.org/packages/ec/38/05340734c33b933fd114f161f25a04e651b0c7c33ab95e9416ade5cb44b8/orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", size = 128871 },
+ { url = "https://files.pythonhosted.org/packages/55/b9/ae8d34899ff0c012039b5a7cb96a389b2476e917733294e498586b45472d/orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", size = 130055 },
+ { url = "https://files.pythonhosted.org/packages/33/aa/6346dd5073730451bee3681d901e3c337e7ec17342fb79659ec9794fc023/orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", size = 129061 },
+ { url = "https://files.pythonhosted.org/packages/39/e4/8eea51598f66a6c853c380979912d17ec510e8e66b280d968602e680b942/orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", size = 136541 },
+ { url = "https://files.pythonhosted.org/packages/9a/47/cb8c654fa9adcc60e99580e17c32b9e633290e6239a99efa6b885aba9dbc/orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", size = 137535 },
+ { url = "https://files.pythonhosted.org/packages/43/92/04b8cc5c2b729f3437ee013ce14a60ab3d3001465d95c184758f19362f23/orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", size = 136703 },
+ { url = "https://files.pythonhosted.org/packages/aa/fd/d0733fcb9086b8be4ebcfcda2d0312865d17d0d9884378b7cffb29d0763f/orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", size = 136293 },
+ { url = "https://files.pythonhosted.org/packages/c2/d7/3c5514e806837c210492d72ae30ccf050ce3f940f45bf085bab272699ef4/orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", size = 140131 },
+ { url = "https://files.pythonhosted.org/packages/9c/dd/ba9d32a53207babf65bd510ac4d0faaa818bd0df9a9c6f472fe7c254f2e3/orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", size = 406164 },
+ { url = "https://files.pythonhosted.org/packages/8e/f9/f68ad68f4af7c7bde57cd514eaa2c785e500477a8bc8f834838eb696a685/orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", size = 149859 },
+ { url = "https://files.pythonhosted.org/packages/b6/d2/7f847761d0c26818395b3d6b21fb6bc2305d94612a35b0a30eae65a22728/orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", size = 139926 },
+ { url = "https://files.pythonhosted.org/packages/9f/37/acd14b12dc62db9a0e1d12386271b8661faae270b22492580d5258808975/orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", size = 136007 },
+ { url = "https://files.pythonhosted.org/packages/c0/a9/967be009ddf0a1fffd7a67de9c36656b28c763659ef91352acc02cbe364c/orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", size = 131314 },
+ { url = "https://files.pythonhosted.org/packages/cb/db/399abd6950fbd94ce125cb8cd1a968def95174792e127b0642781e040ed4/orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", size = 126152 },
+ { url = "https://files.pythonhosted.org/packages/25/e3/54ff63c093cc1697e758e4fceb53164dd2661a7d1bcd522260ba09f54533/orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", size = 243501 },
+ { url = "https://files.pythonhosted.org/packages/ac/7d/e2d1076ed2e8e0ae9badca65bf7ef22710f93887b29eaa37f09850604e09/orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", size = 128862 },
+ { url = "https://files.pythonhosted.org/packages/9f/37/ca2eb40b90621faddfa9517dfe96e25f5ae4d8057a7c0cdd613c17e07b2c/orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", size = 130047 },
+ { url = "https://files.pythonhosted.org/packages/c7/62/1021ed35a1f2bad9040f05fa4cc4f9893410df0ba3eaa323ccf899b1c90a/orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", size = 129073 },
+ { url = "https://files.pythonhosted.org/packages/e8/3f/f84d966ec2a6fd5f73b1a707e7cd876813422ae4bf9f0145c55c9c6a0f57/orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", size = 136597 },
+ { url = "https://files.pythonhosted.org/packages/32/78/4fa0aeca65ee82bbabb49e055bd03fa4edea33f7c080c5c7b9601661ef72/orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", size = 137515 },
+ { url = "https://files.pythonhosted.org/packages/c1/9d/0c102e26e7fde40c4c98470796d050a2ec1953897e2c8ab0cb95b0759fa2/orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", size = 136703 },
+ { url = "https://files.pythonhosted.org/packages/df/ac/2de7188705b4cdfaf0b6c97d2f7849c17d2003232f6e70df98602173f788/orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", size = 136311 },
+ { url = "https://files.pythonhosted.org/packages/e0/52/847fcd1a98407154e944feeb12e3b4d487a0e264c40191fb44d1269cbaa1/orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", size = 140127 },
+ { url = "https://files.pythonhosted.org/packages/c1/ae/21d208f58bdb847dd4d0d9407e2929862561841baa22bdab7aea10ca088e/orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", size = 406201 },
+ { url = "https://files.pythonhosted.org/packages/8d/55/0789d6de386c8366059db098a628e2ad8798069e94409b0d8935934cbcb9/orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", size = 149872 },
+ { url = "https://files.pythonhosted.org/packages/cc/1d/7ff81ea23310e086c17b41d78a72270d9de04481e6113dbe2ac19118f7fb/orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", size = 139931 },
+ { url = "https://files.pythonhosted.org/packages/77/92/25b886252c50ed64be68c937b562b2f2333b45afe72d53d719e46a565a50/orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", size = 136065 },
+ { url = "https://files.pythonhosted.org/packages/63/b8/718eecf0bb7e9d64e4956afaafd23db9f04c776d445f59fe94f54bdae8f0/orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", size = 131310 },
+ { url = "https://files.pythonhosted.org/packages/1a/bf/def5e25d4d8bfce296a9a7c8248109bf58622c21618b590678f945a2c59c/orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", size = 126151 },
+]
+
+[[package]]
+name = "overrides"
+version = "7.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 },
+]
+
+[[package]]
+name = "packaging"
+version = "23.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", size = 146714 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7", size = 53011 },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "python-dateutil" },
+ { name = "pytz" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 },
+ { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 },
+ { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 },
+ { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 },
+ { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 },
+ { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 },
+ { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 },
+ { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 },
+ { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 },
+ { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 },
+ { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 },
+ { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 },
+ { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 },
+ { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635 },
+ { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079 },
+ { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049 },
+ { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638 },
+ { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834 },
+ { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925 },
+ { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071 },
+ { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504 },
+ { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702 },
+ { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535 },
+ { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582 },
+ { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963 },
+ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175 },
+]
+
+[[package]]
+name = "pillow"
+version = "11.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 },
+ { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 },
+ { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 },
+ { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 },
+ { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 },
+ { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 },
+ { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 },
+ { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 },
+ { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 },
+ { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 },
+ { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 },
+ { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 },
+ { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 },
+ { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 },
+ { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 },
+ { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 },
+ { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 },
+ { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 },
+ { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 },
+ { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 },
+ { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 },
+ { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 },
+ { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 },
+ { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 },
+ { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 },
+ { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 },
+ { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 },
+ { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 },
+ { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 },
+ { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 },
+ { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 },
+ { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 },
+ { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 },
+ { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 },
+ { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 },
+ { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 },
+ { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 },
+ { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 },
+ { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 },
+ { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 },
+ { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 },
+ { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 },
+ { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 },
+ { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 },
+ { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 },
+ { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 },
+ { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 },
+]
+
+[[package]]
+name = "posthog"
+version = "5.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backoff" },
+ { name = "distro" },
+ { name = "python-dateutil" },
+ { name = "requests" },
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/20/60ae67bb9d82f00427946218d49e2e7e80fb41c15dc5019482289ec9ce8d/posthog-5.4.0.tar.gz", hash = "sha256:701669261b8d07cdde0276e5bc096b87f9e200e3b9589c5ebff14df658c5893c", size = 88076 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4f/98/e480cab9a08d1c09b1c59a93dade92c1bb7544826684ff2acbfd10fcfbd4/posthog-5.4.0-py3-none-any.whl", hash = "sha256:284dfa302f64353484420b52d4ad81ff5c2c2d1d607c4e2db602ac72761831bd", size = 105364 },
+]
+
+[[package]]
+name = "protobuf"
+version = "6.33.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593 },
+ { url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882 },
+ { url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521 },
+ { url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445 },
+ { url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159 },
+ { url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172 },
+ { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477 },
+]
+
+[[package]]
+name = "pyarrow"
+version = "21.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306 },
+ { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622 },
+ { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094 },
+ { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576 },
+ { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342 },
+ { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218 },
+ { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551 },
+ { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064 },
+ { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837 },
+ { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158 },
+ { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885 },
+ { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625 },
+ { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890 },
+ { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006 },
+]
+
+[[package]]
+name = "pyasn1"
+version = "0.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 },
+]
+
+[[package]]
+name = "pyasn1-modules"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400 },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 },
+]
+
+[[package]]
+name = "pydeck"
+version = "0.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jinja2" },
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
+]
+
+[[package]]
+name = "pypandoc"
+version = "1.16.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/18/9f5f70567b97758625335209b98d5cb857e19aa1a9306e9749567a240634/pypandoc-1.16.2.tar.gz", hash = "sha256:7a72a9fbf4a5dc700465e384c3bb333d22220efc4e972cb98cf6fc723cdca86b", size = 31477 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/e9/b145683854189bba84437ea569bfa786f408c8dc5bc16d8eb0753f5583bf/pypandoc-1.16.2-py3-none-any.whl", hash = "sha256:c200c1139c8e3247baf38d1e9279e85d9f162499d1999c6aa8418596558fe79b", size = 19451 },
+]
+
+[[package]]
+name = "pypdf"
+version = "4.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/65/2ed7c9e1d31d860f096061b3dd2d665f501e09faaa0409a3f0d719d2a16d/pypdf-4.3.1.tar.gz", hash = "sha256:b2f37fe9a3030aa97ca86067a56ba3f9d3565f9a791b305c7355d8392c30d91b", size = 293266 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/60/eccdd92dd4af3e4bea6d6a342f7588c618a15b9bec4b968af581e498bcc4/pypdf-4.3.1-py3-none-any.whl", hash = "sha256:64b31da97eda0771ef22edb1bfecd5deee4b72c3d1736b7df2689805076d6418", size = 295825 },
+]
+
+[[package]]
+name = "pypika"
+version = "0.48.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/2c/94ed7b91db81d61d7096ac8f2d325ec562fc75e35f3baea8749c85b28784/PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378", size = 67259 }
+
+[[package]]
+name = "pyproject-hooks"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216 },
+]
+
+[[package]]
+name = "pyreadline3"
+version = "3.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 },
+]
+
+[[package]]
+name = "regex"
+version = "2025.11.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081 },
+ { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123 },
+ { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814 },
+ { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592 },
+ { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122 },
+ { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272 },
+ { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497 },
+ { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892 },
+ { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462 },
+ { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528 },
+ { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866 },
+ { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189 },
+ { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054 },
+ { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325 },
+ { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984 },
+ { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673 },
+ { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029 },
+ { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437 },
+ { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368 },
+ { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921 },
+ { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708 },
+ { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472 },
+ { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341 },
+ { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666 },
+ { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473 },
+ { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792 },
+ { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214 },
+ { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469 },
+ { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089 },
+ { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059 },
+ { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900 },
+ { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010 },
+ { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893 },
+ { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522 },
+ { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272 },
+ { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958 },
+ { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289 },
+ { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026 },
+ { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499 },
+ { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604 },
+ { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320 },
+ { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372 },
+ { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985 },
+ { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669 },
+ { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030 },
+ { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674 },
+ { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451 },
+ { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980 },
+ { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852 },
+ { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566 },
+ { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463 },
+ { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694 },
+ { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691 },
+ { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583 },
+ { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286 },
+ { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741 },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 },
+]
+
+[[package]]
+name = "requests-oauthlib"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "oauthlib" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 },
+]
+
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 },
+]
+
+[[package]]
+name = "rich"
+version = "14.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393 },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.28.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235 },
+ { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241 },
+ { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079 },
+ { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151 },
+ { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520 },
+ { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699 },
+ { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720 },
+ { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096 },
+ { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465 },
+ { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832 },
+ { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230 },
+ { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268 },
+ { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100 },
+ { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759 },
+ { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326 },
+ { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736 },
+ { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677 },
+ { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847 },
+ { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800 },
+ { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827 },
+ { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471 },
+ { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578 },
+ { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482 },
+ { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447 },
+ { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385 },
+ { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642 },
+ { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507 },
+ { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376 },
+ { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907 },
+ { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830 },
+ { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819 },
+ { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127 },
+ { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767 },
+ { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585 },
+ { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828 },
+ { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509 },
+ { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014 },
+ { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410 },
+ { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593 },
+ { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925 },
+ { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444 },
+ { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968 },
+ { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876 },
+ { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506 },
+ { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433 },
+ { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601 },
+ { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039 },
+ { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407 },
+ { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172 },
+ { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020 },
+ { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451 },
+ { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355 },
+ { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146 },
+ { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656 },
+ { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782 },
+ { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671 },
+ { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749 },
+ { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233 },
+]
+
+[[package]]
+name = "rsa"
+version = "4.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 },
+]
+
+[[package]]
+name = "safetensors"
+version = "0.6.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ac/cc/738f3011628920e027a11754d9cae9abec1aed00f7ae860abbf843755233/safetensors-0.6.2.tar.gz", hash = "sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9", size = 197968 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/b1/3f5fd73c039fc87dba3ff8b5d528bfc5a32b597fea8e7a6a4800343a17c7/safetensors-0.6.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba", size = 454797 },
+ { url = "https://files.pythonhosted.org/packages/8c/c9/bb114c158540ee17907ec470d01980957fdaf87b4aa07914c24eba87b9c6/safetensors-0.6.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b", size = 432206 },
+ { url = "https://files.pythonhosted.org/packages/d3/8e/f70c34e47df3110e8e0bb268d90db8d4be8958a54ab0336c9be4fe86dac8/safetensors-0.6.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd", size = 473261 },
+ { url = "https://files.pythonhosted.org/packages/2a/f5/be9c6a7c7ef773e1996dc214e73485286df1836dbd063e8085ee1976f9cb/safetensors-0.6.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a", size = 485117 },
+ { url = "https://files.pythonhosted.org/packages/c9/55/23f2d0a2c96ed8665bf17a30ab4ce5270413f4d74b6d87dd663258b9af31/safetensors-0.6.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1", size = 616154 },
+ { url = "https://files.pythonhosted.org/packages/98/c6/affb0bd9ce02aa46e7acddbe087912a04d953d7a4d74b708c91b5806ef3f/safetensors-0.6.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda", size = 520713 },
+ { url = "https://files.pythonhosted.org/packages/fe/5d/5a514d7b88e310c8b146e2404e0dc161282e78634d9358975fd56dfd14be/safetensors-0.6.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f", size = 485835 },
+ { url = "https://files.pythonhosted.org/packages/7a/7b/4fc3b2ba62c352b2071bea9cfbad330fadda70579f617506ae1a2f129cab/safetensors-0.6.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19", size = 521503 },
+ { url = "https://files.pythonhosted.org/packages/5a/50/0057e11fe1f3cead9254315a6c106a16dd4b1a19cd247f7cc6414f6b7866/safetensors-0.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce", size = 652256 },
+ { url = "https://files.pythonhosted.org/packages/e9/29/473f789e4ac242593ac1656fbece6e1ecd860bb289e635e963667807afe3/safetensors-0.6.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7", size = 747281 },
+ { url = "https://files.pythonhosted.org/packages/68/52/f7324aad7f2df99e05525c84d352dc217e0fa637a4f603e9f2eedfbe2c67/safetensors-0.6.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5", size = 692286 },
+ { url = "https://files.pythonhosted.org/packages/ad/fe/cad1d9762868c7c5dc70c8620074df28ebb1a8e4c17d4c0cb031889c457e/safetensors-0.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac", size = 655957 },
+ { url = "https://files.pythonhosted.org/packages/59/a7/e2158e17bbe57d104f0abbd95dff60dda916cf277c9f9663b4bf9bad8b6e/safetensors-0.6.2-cp38-abi3-win32.whl", hash = "sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1", size = 308926 },
+ { url = "https://files.pythonhosted.org/packages/2c/c3/c0be1135726618dc1e28d181b8c442403d8dbb9e273fd791de2d4384bcdd/safetensors-0.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c", size = 320192 },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.7.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "joblib" },
+ { name = "numpy" },
+ { name = "scipy" },
+ { name = "threadpoolctl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382 },
+ { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042 },
+ { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180 },
+ { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660 },
+ { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057 },
+ { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731 },
+ { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852 },
+ { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094 },
+ { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436 },
+ { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749 },
+ { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906 },
+ { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836 },
+ { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236 },
+ { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593 },
+ { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007 },
+]
+
+[[package]]
+name = "scipy"
+version = "1.16.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856 },
+ { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306 },
+ { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371 },
+ { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877 },
+ { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103 },
+ { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297 },
+ { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756 },
+ { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566 },
+ { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877 },
+ { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366 },
+ { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931 },
+ { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081 },
+ { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244 },
+ { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753 },
+ { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912 },
+ { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371 },
+ { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477 },
+ { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678 },
+ { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178 },
+ { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246 },
+ { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469 },
+ { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043 },
+ { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952 },
+ { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512 },
+ { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639 },
+ { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729 },
+ { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251 },
+ { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681 },
+ { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423 },
+ { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027 },
+ { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379 },
+ { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052 },
+ { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183 },
+ { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174 },
+ { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852 },
+ { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595 },
+ { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269 },
+ { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779 },
+ { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128 },
+ { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127 },
+]
+
+[[package]]
+name = "sentence-transformers"
+version = "5.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "pillow" },
+ { name = "scikit-learn" },
+ { name = "scipy" },
+ { name = "torch" },
+ { name = "tqdm" },
+ { name = "transformers" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/96/f3f3409179d14dbfdbea8622e2e9eaa3c8836ddcaecd2cd5ff0a11731d20/sentence_transformers-5.1.2.tar.gz", hash = "sha256:0f6c8bd916a78dc65b366feb8d22fd885efdb37432e7630020d113233af2b856", size = 375185 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/a6/a607a737dc1a00b7afe267b9bfde101b8cee2529e197e57471d23137d4e5/sentence_transformers-5.1.2-py3-none-any.whl", hash = "sha256:724ce0ea62200f413f1a5059712aff66495bc4e815a1493f7f9bca242414c333", size = 488009 },
+]
+
+[[package]]
+name = "setuptools"
+version = "80.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
+]
+
+[[package]]
+name = "smmap"
+version = "5.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679 },
+]
+
+[[package]]
+name = "starlette"
+version = "0.49.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340 },
+]
+
+[[package]]
+name = "streamlit"
+version = "1.51.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "altair" },
+ { name = "blinker" },
+ { name = "cachetools" },
+ { name = "click" },
+ { name = "gitpython" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pandas" },
+ { name = "pillow" },
+ { name = "protobuf" },
+ { name = "pyarrow" },
+ { name = "pydeck" },
+ { name = "requests" },
+ { name = "tenacity" },
+ { name = "toml" },
+ { name = "tornado" },
+ { name = "typing-extensions" },
+ { name = "watchdog", marker = "sys_platform != 'darwin'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/59/6d/327ddd5fc35fcf2aeecb4040668337f5565a1c6c95b1e892b8bfd4bb9031/streamlit-1.51.0.tar.gz", hash = "sha256:1e742a9c0b698f466c6f5bf58d333beda5a1fbe8de660743976791b5c1446ef6", size = 9742904 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/60/868371b6482ccd9ef423c6f62650066cf8271fdb2ee84f192695ad6b7a96/streamlit-1.51.0-py3-none-any.whl", hash = "sha256:4008b029f71401ce54946bb09a6a3e36f4f7652cbb48db701224557738cfda38", size = 10171702 },
+]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 },
+]
+
+[[package]]
+name = "tavily-python"
+version = "0.7.13"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx" },
+ { name = "requests" },
+ { name = "tiktoken" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/54/fb/b6d6327a78b9107681701d56b3a2dda48dc5420a6ee8b33a147d87bfba60/tavily_python-0.7.13.tar.gz", hash = "sha256:347f92402331d071557f6dd6680f813a7d484b4ba7240905cc397cd192d1355c", size = 17237 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/4d/e5e4c65cd66144ac3d0d5a6a2bbfba22eb6a63e6e450beba10ee8413b86d/tavily_python-0.7.13-py3-none-any.whl", hash = "sha256:911825467f2bb19b8162b4766d3e81081160a7c0fb8a15c7c716b2bef73e6296", size = 15484 },
+]
+
+[[package]]
+name = "tenacity"
+version = "8.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165 },
+]
+
+[[package]]
+name = "threadpoolctl"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 },
+]
+
+[[package]]
+name = "tiktoken"
+version = "0.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "regex" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802 },
+ { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995 },
+ { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948 },
+ { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986 },
+ { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222 },
+ { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097 },
+ { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117 },
+ { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309 },
+ { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712 },
+ { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725 },
+ { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875 },
+ { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451 },
+ { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794 },
+ { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777 },
+ { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188 },
+ { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978 },
+ { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271 },
+ { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216 },
+ { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860 },
+ { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567 },
+ { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067 },
+ { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473 },
+ { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855 },
+ { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022 },
+ { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736 },
+ { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908 },
+ { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706 },
+ { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667 },
+]
+
+[[package]]
+name = "tokenizers"
+version = "0.22.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318 },
+ { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478 },
+ { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994 },
+ { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141 },
+ { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049 },
+ { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730 },
+ { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560 },
+ { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221 },
+ { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569 },
+ { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599 },
+ { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862 },
+ { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250 },
+ { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003 },
+ { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684 },
+]
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 },
+]
+
+[[package]]
+name = "torch"
+version = "2.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "jinja2" },
+ { name = "networkx" },
+ { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "setuptools" },
+ { name = "sympy" },
+ { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "typing-extensions" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/60/8fc5e828d050bddfab469b3fe78e5ab9a7e53dda9c3bdc6a43d17ce99e63/torch-2.9.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c29455d2b910b98738131990394da3e50eea8291dfeb4b12de71ecf1fdeb21cb", size = 104135743 },
+ { url = "https://files.pythonhosted.org/packages/f2/b7/6d3f80e6918213babddb2a37b46dbb14c15b14c5f473e347869a51f40e1f/torch-2.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:524de44cd13931208ba2c4bde9ec7741fd4ae6bfd06409a604fc32f6520c2bc9", size = 899749493 },
+ { url = "https://files.pythonhosted.org/packages/a6/47/c7843d69d6de8938c1cbb1eba426b1d48ddf375f101473d3e31a5fc52b74/torch-2.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:545844cc16b3f91e08ce3b40e9c2d77012dd33a48d505aed34b7740ed627a1b2", size = 110944162 },
+ { url = "https://files.pythonhosted.org/packages/28/0e/2a37247957e72c12151b33a01e4df651d9d155dd74d8cfcbfad15a79b44a/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5be4bf7496f1e3ffb1dd44b672adb1ac3f081f204c5ca81eba6442f5f634df8e", size = 74830751 },
+ { url = "https://files.pythonhosted.org/packages/4b/f7/7a18745edcd7b9ca2381aa03353647bca8aace91683c4975f19ac233809d/torch-2.9.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:30a3e170a84894f3652434b56d59a64a2c11366b0ed5776fab33c2439396bf9a", size = 104142929 },
+ { url = "https://files.pythonhosted.org/packages/f4/dd/f1c0d879f2863ef209e18823a988dc7a1bf40470750e3ebe927efdb9407f/torch-2.9.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2", size = 899748978 },
+ { url = "https://files.pythonhosted.org/packages/1f/9f/6986b83a53b4d043e36f3f898b798ab51f7f20fdf1a9b01a2720f445043d/torch-2.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db", size = 111176995 },
+ { url = "https://files.pythonhosted.org/packages/40/60/71c698b466dd01e65d0e9514b5405faae200c52a76901baf6906856f17e4/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587", size = 74480347 },
+ { url = "https://files.pythonhosted.org/packages/48/50/c4b5112546d0d13cc9eaa1c732b823d676a9f49ae8b6f97772f795874a03/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a", size = 74433245 },
+ { url = "https://files.pythonhosted.org/packages/81/c9/2628f408f0518b3bae49c95f5af3728b6ab498c8624ab1e03a43dd53d650/torch-2.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6", size = 104134804 },
+ { url = "https://files.pythonhosted.org/packages/28/fc/5bc91d6d831ae41bf6e9e6da6468f25330522e92347c9156eb3f1cb95956/torch-2.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9", size = 899747132 },
+ { url = "https://files.pythonhosted.org/packages/63/5d/e8d4e009e52b6b2cf1684bde2a6be157b96fb873732542fb2a9a99e85a83/torch-2.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d", size = 110934845 },
+ { url = "https://files.pythonhosted.org/packages/bd/b2/2d15a52516b2ea3f414643b8de68fa4cb220d3877ac8b1028c83dc8ca1c4/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c", size = 74823558 },
+ { url = "https://files.pythonhosted.org/packages/86/5c/5b2e5d84f5b9850cd1e71af07524d8cbb74cba19379800f1f9f7c997fc70/torch-2.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7", size = 104145788 },
+ { url = "https://files.pythonhosted.org/packages/a9/8c/3da60787bcf70add986c4ad485993026ac0ca74f2fc21410bc4eb1bb7695/torch-2.9.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73", size = 899735500 },
+ { url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659 },
+]
+
+[[package]]
+name = "tornado"
+version = "6.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563 },
+ { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729 },
+ { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295 },
+ { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644 },
+ { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878 },
+ { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549 },
+ { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973 },
+ { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954 },
+ { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023 },
+ { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427 },
+ { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456 },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 },
+]
+
+[[package]]
+name = "transformers"
+version = "4.57.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "regex" },
+ { name = "requests" },
+ { name = "safetensors" },
+ { name = "tokenizers" },
+ { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d6/68/a39307bcc4116a30b2106f2e689130a48de8bd8a1e635b5e1030e46fcd9e/transformers-4.57.1.tar.gz", hash = "sha256:f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55", size = 10142511 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/d3/c16c3b3cf7655a67db1144da94b021c200ac1303f82428f2beef6c2e72bb/transformers-4.57.1-py3-none-any.whl", hash = "sha256:b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267", size = 11990925 },
+]
+
+[[package]]
+name = "triton"
+version = "3.5.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/46/8c3bbb5b0a19313f50edcaa363b599e5a1a5ac9683ead82b9b80fe497c8d/triton-3.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba", size = 170470410 },
+ { url = "https://files.pythonhosted.org/packages/37/92/e97fcc6b2c27cdb87ce5ee063d77f8f26f19f06916aa680464c8104ef0f6/triton-3.5.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8", size = 170579924 },
+ { url = "https://files.pythonhosted.org/packages/a4/e6/c595c35e5c50c4bc56a7bac96493dad321e9e29b953b526bbbe20f9911d0/triton-3.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60", size = 170480488 },
+ { url = "https://files.pythonhosted.org/packages/16/b5/b0d3d8b901b6a04ca38df5e24c27e53afb15b93624d7fd7d658c7cd9352a/triton-3.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478", size = 170582192 },
+]
+
+[[package]]
+name = "typer"
+version = "0.20.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
+]
+
+[[package]]
+name = "upthink"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "httpx" },
+ { name = "langchain-chroma" },
+ { name = "langchain-core" },
+ { name = "langchain-upstage" },
+ { name = "markdown" },
+ { name = "openai" },
+ { name = "pypandoc" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "sentence-transformers" },
+ { name = "streamlit" },
+ { name = "tavily-python" },
+ { name = "transformers" },
+ { name = "wikipedia" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "httpx", specifier = ">=0.28.1" },
+ { name = "langchain-chroma", specifier = "<1.0.0" },
+ { name = "langchain-core", specifier = ">=0.2.43" },
+ { name = "langchain-upstage", specifier = ">=0.1.7" },
+ { name = "markdown", specifier = ">=3.10" },
+ { name = "openai", specifier = ">=1.109.1" },
+ { name = "pypandoc", specifier = ">=1.16.2" },
+ { name = "python-dotenv", specifier = ">=1.2.1" },
+ { name = "pyyaml", specifier = ">=6.0.3" },
+ { name = "sentence-transformers", specifier = ">=5.1.2" },
+ { name = "streamlit", specifier = ">=1.50.0" },
+ { name = "tavily-python", specifier = ">=0.7.13" },
+ { name = "transformers", specifier = ">=4.57.1" },
+ { name = "wikipedia", specifier = ">=1.4.0" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109 },
+]
+
+[package.optional-dependencies]
+standard = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "httptools" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
+ { name = "watchfiles" },
+ { name = "websockets" },
+]
+
+[[package]]
+name = "uvloop"
+version = "0.22.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611 },
+ { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811 },
+ { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562 },
+ { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890 },
+ { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472 },
+ { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051 },
+ { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067 },
+ { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423 },
+ { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437 },
+ { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101 },
+ { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158 },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360 },
+ { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790 },
+ { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783 },
+ { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548 },
+ { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065 },
+ { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384 },
+ { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730 },
+]
+
+[[package]]
+name = "watchdog"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 },
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 },
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 },
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 },
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 },
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 },
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 },
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 },
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321 },
+ { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783 },
+ { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279 },
+ { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405 },
+ { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976 },
+ { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506 },
+ { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936 },
+ { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147 },
+ { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007 },
+ { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280 },
+ { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056 },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162 },
+ { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909 },
+ { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389 },
+ { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964 },
+ { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114 },
+ { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264 },
+ { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877 },
+ { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176 },
+ { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577 },
+ { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425 },
+ { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826 },
+ { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208 },
+ { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315 },
+ { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869 },
+ { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919 },
+ { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845 },
+ { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027 },
+ { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615 },
+ { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836 },
+ { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099 },
+ { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626 },
+ { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519 },
+ { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078 },
+ { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664 },
+ { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154 },
+ { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820 },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510 },
+ { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408 },
+ { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968 },
+ { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096 },
+ { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040 },
+ { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847 },
+ { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072 },
+ { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104 },
+ { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112 },
+]
+
+[[package]]
+name = "websocket-client"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616 },
+]
+
+[[package]]
+name = "websockets"
+version = "15.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 },
+ { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 },
+ { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 },
+ { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 },
+ { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 },
+ { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 },
+ { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 },
+ { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 },
+ { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
+ { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
+ { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
+ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
+]
+
+[[package]]
+name = "wikipedia"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beautifulsoup4" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/35/25e68fbc99e672127cc6fbb14b8ec1ba3dfef035bf1e4c90f78f24a80b7d/wikipedia-1.4.0.tar.gz", hash = "sha256:db0fad1829fdd441b1852306e9856398204dc0786d2996dd2e0c8bb8e26133b2", size = 27748 }
+
+[[package]]
+name = "wrapt"
+version = "1.17.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 },
+ { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 },
+ { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 },
+ { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 },
+ { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 },
+ { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 },
+ { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 },
+ { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 },
+ { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 },
+ { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 },
+ { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132 },
+ { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091 },
+ { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172 },
+ { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163 },
+ { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963 },
+ { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945 },
+ { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857 },
+ { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178 },
+ { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310 },
+ { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266 },
+ { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544 },
+ { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283 },
+ { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366 },
+ { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571 },
+ { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094 },
+ { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659 },
+ { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946 },
+ { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717 },
+ { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334 },
+ { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471 },
+ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 },
+]
+
+[[package]]
+name = "zipp"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 },
+]