diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 50b8ef2f9..fcea26923 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -11,13 +11,13 @@ on: env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} - + concurrency: # This concurrency group ensures that only one job in the group runs at a time. # If a new job is triggered, the previous one will be canceled. group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true - + jobs: build-and-push: strategy: @@ -39,7 +39,7 @@ jobs: platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV echo "GHCR_IMAGE=ghcr.io/${GITHUB_REPOSITORY@L}" >> $GITHUB_ENV - + - name: Checkout repository uses: actions/checkout@v4 @@ -75,21 +75,21 @@ jobs: outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }},oci-mediatypes=true cache-from: type=gha,scope=${{ github.repository }}-${{ github.ref_name }}-${{ matrix.platform }} cache-to: type=gha,mode=max,scope=${{ github.repository }}-${{ github.ref_name }}-${{ matrix.platform }} - + - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - - name: Upload digest + - name: Upload digest uses: actions/upload-artifact@v4 with: name: digests-${{ env.PLATFORM_PAIR }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 - + merge: name: merge Docker manifests runs-on: ubuntu-latest @@ -105,7 +105,7 @@ jobs: id: prepare run: | echo "GHCR_IMAGE=ghcr.io/${GITHUB_REPOSITORY@L}" >> $GITHUB_ENV - + - name: Download digests uses: actions/download-artifact@v4 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..9a4e4eba5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,71 @@ +# Pre-commit hooks for deepwiki-open (Next.js frontend + FastAPI backend) +# +# Setup: +# pipx install pre-commit # or: pip install pre-commit +# pre-commit install # install the git hook +# pre-commit run --all-files # run once across the whole repo +# +# Update pinned versions later with: pre-commit autoupdate + +default_language_version: + python: python3.12 + +# Never touch generated / vendored / log / backup files. +exclude: | + (?x)^( + .*\.bak| + api/logs/.*| + .*/__pycache__/.*| + public/.*\.svg| + yarn\.lock| + package-lock\.json| + uv\.lock| + api/poetry\.lock + )$ + +repos: + # --------------------------------------------------------------------------- + # General hygiene + # --------------------------------------------------------------------------- + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] + - id: check-merge-conflict + - id: check-case-conflict + - id: check-added-large-files + args: [--maxkb=1024] + - id: check-yaml + - id: check-toml + - id: check-json + # .vscode/*.json and tsconfig.json are JSONC (allow comments) -> skip + exclude: ^(\.vscode/|tsconfig\.json$) + - id: detect-private-key + + # --------------------------------------------------------------------------- + # Secret scanning + # Blocks commits that contain tokens, keys, or credentials. + # --------------------------------------------------------------------------- + - repo: https://github.com/gitleaks/gitleaks + rev: v8.21.2 + hooks: + - id: gitleaks + + # --------------------------------------------------------------------------- + # Python — ruff replaces flake8 + isort + black (lint + format in one tool). + # Scoped to the backend and test dirs only. + # --------------------------------------------------------------------------- + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.6 + hooks: + - id: ruff + name: ruff (lint + autofix) + args: [--fix] + files: ^(api|test)/.*\.py$ # disable tests folders for now + - id: ruff-format + name: ruff (format) + files: ^(api|test)/.*\.py$ diff --git a/Ollama-instruction.md b/Ollama-instruction.md index f5332dac3..5b90faba0 100644 --- a/Ollama-instruction.md +++ b/Ollama-instruction.md @@ -94,7 +94,7 @@ cp api/config/embedder.ollama.json.bak api/config/embedder.json -v ~/.adalflow:/root/.adalflow \ -e OLLAMA_HOST=your_ollama_host \ deepwiki:ollama-local - + # For local repository analysis docker run -p 3000:3000 -p 8001:8001 --name deepwiki \ -v ~/.adalflow:/root/.adalflow \ diff --git a/README.es.md b/README.es.md index d7a3f71a0..2ef6dca14 100644 --- a/README.es.md +++ b/README.es.md @@ -8,6 +8,7 @@ 2. Generará documentación completa 3. Creará diagramas visuales para explicar cómo funciona todo 4. Organizará todo en una wiki fácil de navegar +5. Generará un mapa de código (codemap) para recorridos guiados centrados en el código [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing) @@ -289,6 +290,9 @@ Para usar Investigación Profunda, simplemente activa el interruptor "Investigac ![Función de Investigación Profunda](screenshots/DeepResearch.png) *Investigación Profunda realiza investigaciones de múltiples turnos para temas complejos* +![Codemap](screenshots/Codemap.png) +*Codemap* + ### Video de Demostración [![Video de Demostración de DeepWiki](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4) @@ -331,4 +335,3 @@ Este proyecto está licenciado bajo la Licencia MIT - consulta el archivo [LICEN ## ⭐ Historial de Estrellas [![Gráfico de Historial de Estrellas](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date) - diff --git a/README.fr.md b/README.fr.md index 248ca5624..d71f30944 100644 --- a/README.fr.md +++ b/README.fr.md @@ -9,6 +9,7 @@ 2. Génère une documentation complète 3. Crée des diagrammes visuels pour expliquer le fonctionnement 4. Organise le tout dans un wiki facile à naviguer +5. Génère une codemap pour des visites guidées centrées sur le code [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing) [![Tip in Crypto](https://tip.md/badge.svg)](https://tip.md/sng-asyncfunc) @@ -514,6 +515,9 @@ Pour utiliser DeepResearch, activez simplement le commutateur "Deep Research" da ![Fonctionnalité DeepResearch](screenshots/DeepResearch.png) *DeepResearch effectue des recherches en plusieurs étapes pour des sujets complexes* +![Codemap](screenshots/Codemap.png) +*Codemap* + ### Vidéo de démonstration [![Vidéo de démo DeepWiki](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4) @@ -560,5 +564,3 @@ Projet sous licence MIT – Voir le fichier [LICENSE](LICENSE). ## ⭐ Historique des stars [![Historique des stars](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date) - - diff --git a/README.ja.md b/README.ja.md index bb9e42027..b427fb521 100644 --- a/README.ja.md +++ b/README.ja.md @@ -8,6 +8,7 @@ 2. 包括的なドキュメントを生成 3. すべての仕組みを説明する視覚的な図を作成 4. すべてを簡単に閲覧できる Wiki に整理 +5. コード中心のガイドツアーのためのコードマップを生成 [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing) @@ -397,6 +398,9 @@ _個人アクセストークンを使用したプライベートリポジトリ ![詳細調査機能](screenshots/DeepResearch.png) _詳細調査は複雑なトピックに対して多段階の調査を実施_ +![Codemap](screenshots/Codemap.png) +*Codemap* + ### デモビデオ [![DeepWikiデモビデオ](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4) @@ -444,4 +448,3 @@ _DeepWiki の動作を見る!_ ## ⭐ スター履歴 [![スター履歴チャート](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date) - diff --git a/README.kr.md b/README.kr.md index c3c61b655..1fea276fb 100644 --- a/README.kr.md +++ b/README.kr.md @@ -8,6 +8,7 @@ 2. 포괄적인 문서 생성 3. 모든 작동 방식을 설명하는 시각적 다이어그램 생성 4. 이를 쉽게 탐색할 수 있는 위키로 정리 +5. 코드 중심의 가이드 투어를 위한 코드맵 생성 [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing) @@ -385,6 +386,9 @@ DeepResearch를 사용하려면 질문 제출 전 Ask 인터페이스에서 "Dee ![DeepResearch Feature](screenshots/DeepResearch.png) *DeepResearch는 복잡한 주제에 대해 다중 턴 조사를 수행* +![Codemap](screenshots/Codemap.png) +*Codemap* + ### 데모 영상 [![DeepWiki Demo Video](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4) @@ -427,4 +431,3 @@ DeepResearch를 사용하려면 질문 제출 전 Ask 인터페이스에서 "Dee ## ⭐ 스타 히스토리 [![Star History Chart](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date) - diff --git a/README.md b/README.md index 8abbe6740..806c0f0d4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ 2. Generate comprehensive documentation 3. Create visual diagrams to explain how everything works 4. Organize it all into an easy-to-navigate wiki +5. Generate codemap for code-centric guided tours [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing) [![Tip in Crypto](https://tip.md/badge.svg)](https://tip.md/sng-asyncfunc) diff --git a/README.pt-br.md b/README.pt-br.md index eee4044ca..acc133588 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -8,6 +8,7 @@ 2. Gerar documentação abrangente 3. Criar diagramas visuais para explicar como tudo funciona 4. Organizar tudo em uma wiki fácil de navegar +5. Gerar um codemap para tours guiados centrados no código [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing) [![Tip in Crypto](https://tip.md/badge.svg)](https://tip.md/sng-asyncfunc) @@ -297,7 +298,7 @@ docker-compose up | Variável | Descrição | Obrigatória | Observação | |----------------------|--------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------| -| `GOOGLE_API_KEY` | Chave API Google Gemini para geração com IA | Não | Necessária apenas se você quiser usar modelos Google Gemini +| `GOOGLE_API_KEY` | Chave API Google Gemini para geração com IA | Não | Necessária apenas se você quiser usar modelos Google Gemini | `OPENAI_API_KEY` | Chave API OpenAI para embeddings | Sim | Nota: Isso é necessário mesmo se você não estiver usando modelos OpenAI, pois é usado para embeddings. | | `OPENROUTER_API_KEY` | Chave API OpenRouter para modelos alternativos | Não | Necessária apenas se você quiser usar modelos OpenRouter | | `OLLAMA_HOST` | Host Ollama (padrão: http://localhost:11434) | Não | Necessária apenas se você quiser usar servidor Ollama externo | @@ -466,6 +467,9 @@ Para usar o DeepResearch, simplesmente alterne o interruptor "Pesquisa Aprofunda ![Recurso DeepResearch](screenshots/DeepResearch.png) *DeepResearch conduz investigações em várias etapas para tópicos complexos* +![Codemap](screenshots/Codemap.png) +*Codemap* + ### Vídeo de Demonstração [![Vídeo de Demonstração do DeepWiki](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4) diff --git a/README.ru.md b/README.ru.md index 01c5c9dcf..316f93eeb 100644 --- a/README.ru.md +++ b/README.ru.md @@ -8,6 +8,7 @@ 2. Генерацию полноценной документации 3. Построение визуальных диаграмм, объясняющих работу системы 4. Организацию всего в удобную и структурированную вики +5. Создание карты кода (codemap) для пошаговых экскурсий по коду [!["Купить мне кофе"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing) [![Поддержать в криптовалюте](https://tip.md/badge.svg)](https://tip.md/sng-asyncfunc) @@ -461,6 +462,9 @@ docker build --build-arg CUSTOM_CERT_DIR=certs . ![DeepResearch](screenshots/DeepResearch.png) *DeepResearch анализирует сложные темы* +![Codemap](screenshots/Codemap.png) +*Codemap* + ### Видео-демо [![Видео](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4) diff --git a/README.vi.md b/README.vi.md index 38bd54fd0..e8c022598 100644 --- a/README.vi.md +++ b/README.vi.md @@ -8,6 +8,7 @@ 2. Tạo tài liệu đầy đủ và chi tiết 3. Tạo sơ đồ trực quan để giải thích cách mọi thứ hoạt động 4. Sắp xếp tất cả documents thành một wiki dễ hiểu +5. Tạo codemap cho các chuyến tham quan có hướng dẫn tập trung vào mã nguồn [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing) @@ -377,6 +378,9 @@ DeepResearch nâng tầm phân tích kho mã với quy trình nghiện cứu nhi ![Tính năng DeepResearch](screenshots/DeepResearch.png) *DeepResearch thực hiện nghiện cứu nhiểu vòng cho các chủ đề phức tạp* +![Codemap](screenshots/Codemap.png) +*Codemap* + ### Demo Video [![DeepWiki Demo Video](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4) @@ -420,4 +424,3 @@ Dự án này được cấp phép theo Giấy phép MIT - xem file [LICENSE](LI ## ⭐ Lịch sử [![Biểu đồ lịch sử](https://api.star-history.com/svg?repos=AsyncFuncAI/deepwiki-open&type=Date)](https://star-history.com/#AsyncFuncAI/deepwiki-open&Date) - diff --git a/README.zh-tw.md b/README.zh-tw.md index cc29aefcb..2a736b2c4 100644 --- a/README.zh-tw.md +++ b/README.zh-tw.md @@ -8,6 +8,7 @@ 2. 產生全面的文件 3. 建立視覺化圖表解釋一切如何運作 4. 將所有內容整理成易於導覽的 Wiki +5. 產生程式碼地圖,提供以程式碼為中心的引導式導覽 [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing) [![Tip in Crypto](https://tip.md/badge.svg)](https://tip.md/sng-asyncfunc) @@ -304,7 +305,7 @@ docker-compose up | 變數 | 說明 | 必需 | 備註 | |----------------------|--------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------| -| `GOOGLE_API_KEY` | Google Gemini API 金鑰,用於 AI 產生 | 否 | 只有在您想使用 Google Gemini 模型時才需要 +| `GOOGLE_API_KEY` | Google Gemini API 金鑰,用於 AI 產生 | 否 | 只有在您想使用 Google Gemini 模型時才需要 | `OPENAI_API_KEY` | OpenAI API 金鑰,用於嵌入 | 是 | 備註:即使您不使用 OpenAI 模型,這個也是必需的,因為它用於嵌入 | | `OPENROUTER_API_KEY` | OpenRouter API 金鑰,用於替代模型 | 否 | 只有在您想使用 OpenRouter 模型時才需要 | | `OLLAMA_HOST` | Ollama 主機(預設:http://localhost:11434) | 否 | 只有在您想使用外部 Ollama 伺服器時才需要 | @@ -466,16 +467,16 @@ OpenRouter 特別適用於以下情況: ## 📱 螢幕截圖 ### 主頁面 -![主頁面](screenshots/home.png) +![主頁面](./screenshots/Interface.png) ### Wiki 頁面 -![Wiki 頁面](screenshots/wiki-page.png) - -### 提問功能 -![提問功能](screenshots/ask.png) +![Wiki 頁面](screenshots/Deepwiki.png) ### 深度研究 -![深度研究](screenshots/deep-research.png) +![深度研究](screenshots/DeepResearch.png) + +### Codemap +![Codemap](screenshots/Codemap.png) ### 展示影片 diff --git a/README.zh.md b/README.zh.md index ac7dc2db9..247be2910 100644 --- a/README.zh.md +++ b/README.zh.md @@ -8,6 +8,7 @@ 2. 生成全面的文档 3. 创建可视化图表解释一切如何运作 4. 将所有内容整理成易于导航的Wiki +5. 生成代码地图,提供以代码为中心的引导式导览 [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/sheing) @@ -207,6 +208,9 @@ deepwiki/ ![深度研究功能](screenshots/DeepResearch.png) *深度研究为复杂主题进行多轮调查* +![Codemap](screenshots/Codemap.png) +*代码地图* + ### 演示视频 [![DeepWiki演示视频](https://img.youtube.com/vi/zGANs8US8B4/0.jpg)](https://youtu.be/zGANs8US8B4) @@ -382,4 +386,3 @@ OpenAI 客户端的 base_url 配置主要为拥有私有 API 渠道的企业用 3. 程序会自动用环境变量的值替换 embedder.json 里的占位符。 这样即可无缝切换到 OpenAI 兼容的 embedding 服务,无需修改代码。 - diff --git a/api/api.py b/api/api.py deleted file mode 100644 index d40e73f96..000000000 --- a/api/api.py +++ /dev/null @@ -1,634 +0,0 @@ -import os -import logging -from fastapi import FastAPI, HTTPException, Query, Request, WebSocket -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, Response -from typing import List, Optional, Dict, Any, Literal -import json -from datetime import datetime -from pydantic import BaseModel, Field -import google.generativeai as genai -import asyncio - -# Configure logging -from api.logging_config import setup_logging - -setup_logging() -logger = logging.getLogger(__name__) - - -# Initialize FastAPI app -app = FastAPI( - title="Streaming API", - description="API for streaming chat completions" -) - -# Configure CORS -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # Allows all origins - allow_credentials=True, - allow_methods=["*"], # Allows all methods - allow_headers=["*"], # Allows all headers -) - -# Helper function to get adalflow root path -def get_adalflow_default_root_path(): - return os.path.expanduser(os.path.join("~", ".adalflow")) - -# --- Pydantic Models --- -class WikiPage(BaseModel): - """ - Model for a wiki page. - """ - id: str - title: str - content: str - filePaths: List[str] - importance: str # Should ideally be Literal['high', 'medium', 'low'] - relatedPages: List[str] - -class ProcessedProjectEntry(BaseModel): - id: str # Filename - owner: str - repo: str - name: str # owner/repo - repo_type: str # Renamed from type to repo_type for clarity with existing models - submittedAt: int # Timestamp - language: str # Extracted from filename - -class RepoInfo(BaseModel): - owner: str - repo: str - type: str - token: Optional[str] = None - localPath: Optional[str] = None - repoUrl: Optional[str] = None - - -class WikiSection(BaseModel): - """ - Model for the wiki sections. - """ - id: str - title: str - pages: List[str] - subsections: Optional[List[str]] = None - - -class WikiStructureModel(BaseModel): - """ - Model for the overall wiki structure. - """ - id: str - title: str - description: str - pages: List[WikiPage] - sections: Optional[List[WikiSection]] = None - rootSections: Optional[List[str]] = None - -class WikiCacheData(BaseModel): - """ - Model for the data to be stored in the wiki cache. - """ - wiki_structure: WikiStructureModel - generated_pages: Dict[str, WikiPage] - repo_url: Optional[str] = None #compatible for old cache - repo: Optional[RepoInfo] = None - provider: Optional[str] = None - model: Optional[str] = None - -class WikiCacheRequest(BaseModel): - """ - Model for the request body when saving wiki cache. - """ - repo: RepoInfo - language: str - wiki_structure: WikiStructureModel - generated_pages: Dict[str, WikiPage] - provider: str - model: str - -class WikiExportRequest(BaseModel): - """ - Model for requesting a wiki export. - """ - repo_url: str = Field(..., description="URL of the repository") - pages: List[WikiPage] = Field(..., description="List of wiki pages to export") - format: Literal["markdown", "json"] = Field(..., description="Export format (markdown or json)") - -# --- Model Configuration Models --- -class Model(BaseModel): - """ - Model for LLM model configuration - """ - id: str = Field(..., description="Model identifier") - name: str = Field(..., description="Display name for the model") - -class Provider(BaseModel): - """ - Model for LLM provider configuration - """ - id: str = Field(..., description="Provider identifier") - name: str = Field(..., description="Display name for the provider") - models: List[Model] = Field(..., description="List of available models for this provider") - supportsCustomModel: Optional[bool] = Field(False, description="Whether this provider supports custom models") - -class ModelConfig(BaseModel): - """ - Model for the entire model configuration - """ - providers: List[Provider] = Field(..., description="List of available model providers") - defaultProvider: str = Field(..., description="ID of the default provider") - -class AuthorizationConfig(BaseModel): - code: str = Field(..., description="Authorization code") - -from api.config import configs, WIKI_AUTH_MODE, WIKI_AUTH_CODE - -@app.get("/lang/config") -async def get_lang_config(): - return configs["lang_config"] - -@app.get("/auth/status") -async def get_auth_status(): - """ - Check if authentication is required for the wiki. - """ - return {"auth_required": WIKI_AUTH_MODE} - -@app.post("/auth/validate") -async def validate_auth_code(request: AuthorizationConfig): - """ - Check authorization code. - """ - return {"success": WIKI_AUTH_CODE == request.code} - -@app.get("/models/config", response_model=ModelConfig) -async def get_model_config(): - """ - Get available model providers and their models. - - This endpoint returns the configuration of available model providers and their - respective models that can be used throughout the application. - - Returns: - ModelConfig: A configuration object containing providers and their models - """ - try: - logger.info("Fetching model configurations") - - # Create providers from the config file - providers = [] - default_provider = configs.get("default_provider", "google") - - # Add provider configuration based on config.py - for provider_id, provider_config in configs["providers"].items(): - models = [] - # Add models from config - for model_id in provider_config["models"].keys(): - # Get a more user-friendly display name if possible - models.append(Model(id=model_id, name=model_id)) - - # Add provider with its models - providers.append( - Provider( - id=provider_id, - name=f"{provider_id.capitalize()}", - supportsCustomModel=provider_config.get("supportsCustomModel", False), - models=models - ) - ) - - # Create and return the full configuration - config = ModelConfig( - providers=providers, - defaultProvider=default_provider - ) - return config - - except Exception as e: - logger.error(f"Error creating model configuration: {str(e)}") - # Return some default configuration in case of error - return ModelConfig( - providers=[ - Provider( - id="google", - name="Google", - supportsCustomModel=True, - models=[ - Model(id="gemini-2.5-flash", name="Gemini 2.5 Flash") - ] - ) - ], - defaultProvider="google" - ) - -@app.post("/export/wiki") -async def export_wiki(request: WikiExportRequest): - """ - Export wiki content as Markdown or JSON. - - Args: - request: The export request containing wiki pages and format - - Returns: - A downloadable file in the requested format - """ - try: - logger.info(f"Exporting wiki for {request.repo_url} in {request.format} format") - - # Extract repository name from URL for the filename - repo_parts = request.repo_url.rstrip('/').split('/') - repo_name = repo_parts[-1] if len(repo_parts) > 0 else "wiki" - - # Get current timestamp for the filename - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - - if request.format == "markdown": - # Generate Markdown content - content = generate_markdown_export(request.repo_url, request.pages) - filename = f"{repo_name}_wiki_{timestamp}.md" - media_type = "text/markdown" - else: # JSON format - # Generate JSON content - content = generate_json_export(request.repo_url, request.pages) - filename = f"{repo_name}_wiki_{timestamp}.json" - media_type = "application/json" - - # Create response with appropriate headers for file download - response = Response( - content=content, - media_type=media_type, - headers={ - "Content-Disposition": f"attachment; filename={filename}" - } - ) - - return response - - except Exception as e: - error_msg = f"Error exporting wiki: {str(e)}" - logger.error(error_msg) - raise HTTPException(status_code=500, detail=error_msg) - -@app.get("/local_repo/structure") -async def get_local_repo_structure(path: str = Query(None, description="Path to local repository")): - """Return the file tree and README content for a local repository.""" - if not path: - return JSONResponse( - status_code=400, - content={"error": "No path provided. Please provide a 'path' query parameter."} - ) - - if not os.path.isdir(path): - return JSONResponse( - status_code=404, - content={"error": f"Directory not found: {path}"} - ) - - try: - logger.info(f"Processing local repository at: {path}") - file_tree_lines = [] - readme_content = "" - - for root, dirs, files in os.walk(path): - # Exclude hidden dirs/files and virtual envs - dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__' and d != 'node_modules' and d != '.venv'] - for file in files: - if file.startswith('.') or file == '__init__.py' or file == '.DS_Store': - continue - rel_dir = os.path.relpath(root, path) - rel_file = os.path.join(rel_dir, file) if rel_dir != '.' else file - file_tree_lines.append(rel_file) - # Find README.md (case-insensitive) - if file.lower() == 'readme.md' and not readme_content: - try: - with open(os.path.join(root, file), 'r', encoding='utf-8') as f: - readme_content = f.read() - except Exception as e: - logger.warning(f"Could not read README.md: {str(e)}") - readme_content = "" - - file_tree_str = '\n'.join(sorted(file_tree_lines)) - return {"file_tree": file_tree_str, "readme": readme_content} - except Exception as e: - logger.error(f"Error processing local repository: {str(e)}") - return JSONResponse( - status_code=500, - content={"error": f"Error processing local repository: {str(e)}"} - ) - -def generate_markdown_export(repo_url: str, pages: List[WikiPage]) -> str: - """ - Generate Markdown export of wiki pages. - - Args: - repo_url: The repository URL - pages: List of wiki pages - - Returns: - Markdown content as string - """ - # Start with metadata - markdown = f"# Wiki Documentation for {repo_url}\n\n" - markdown += f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" - - # Add table of contents - markdown += "## Table of Contents\n\n" - for page in pages: - markdown += f"- [{page.title}](#{page.id})\n" - markdown += "\n" - - # Add each page - for page in pages: - markdown += f"\n\n" - markdown += f"## {page.title}\n\n" - - - - # Add related pages - if page.relatedPages and len(page.relatedPages) > 0: - markdown += "### Related Pages\n\n" - related_titles = [] - for related_id in page.relatedPages: - # Find the title of the related page - related_page = next((p for p in pages if p.id == related_id), None) - if related_page: - related_titles.append(f"[{related_page.title}](#{related_id})") - - if related_titles: - markdown += "Related topics: " + ", ".join(related_titles) + "\n\n" - - # Add page content - markdown += f"{page.content}\n\n" - markdown += "---\n\n" - - return markdown - -def generate_json_export(repo_url: str, pages: List[WikiPage]) -> str: - """ - Generate JSON export of wiki pages. - - Args: - repo_url: The repository URL - pages: List of wiki pages - - Returns: - JSON content as string - """ - # Create a dictionary with metadata and pages - export_data = { - "metadata": { - "repository": repo_url, - "generated_at": datetime.now().isoformat(), - "page_count": len(pages) - }, - "pages": [page.model_dump() for page in pages] - } - - # Convert to JSON string with pretty formatting - return json.dumps(export_data, indent=2) - -# Import the simplified chat implementation -from api.simple_chat import chat_completions_stream -from api.websocket_wiki import handle_websocket_chat - -# Add the chat_completions_stream endpoint to the main app -app.add_api_route("/chat/completions/stream", chat_completions_stream, methods=["POST"]) - -# Add the WebSocket endpoint -app.add_websocket_route("/ws/chat", handle_websocket_chat) - -# --- Wiki Cache Helper Functions --- - -WIKI_CACHE_DIR = os.path.join(get_adalflow_default_root_path(), "wikicache") -os.makedirs(WIKI_CACHE_DIR, exist_ok=True) - -def get_wiki_cache_path(owner: str, repo: str, repo_type: str, language: str) -> str: - """Generates the file path for a given wiki cache.""" - filename = f"deepwiki_cache_{repo_type}_{owner}_{repo}_{language}.json" - return os.path.join(WIKI_CACHE_DIR, filename) - -async def read_wiki_cache(owner: str, repo: str, repo_type: str, language: str) -> Optional[WikiCacheData]: - """Reads wiki cache data from the file system.""" - cache_path = get_wiki_cache_path(owner, repo, repo_type, language) - if os.path.exists(cache_path): - try: - with open(cache_path, 'r', encoding='utf-8') as f: - data = json.load(f) - return WikiCacheData(**data) - except Exception as e: - logger.error(f"Error reading wiki cache from {cache_path}: {e}") - return None - return None - -async def save_wiki_cache(data: WikiCacheRequest) -> bool: - """Saves wiki cache data to the file system.""" - cache_path = get_wiki_cache_path(data.repo.owner, data.repo.repo, data.repo.type, data.language) - logger.info(f"Attempting to save wiki cache. Path: {cache_path}") - try: - payload = WikiCacheData( - wiki_structure=data.wiki_structure, - generated_pages=data.generated_pages, - repo=data.repo, - provider=data.provider, - model=data.model - ) - # Log size of data to be cached for debugging (avoid logging full content if large) - try: - payload_json = payload.model_dump_json() - payload_size = len(payload_json.encode('utf-8')) - logger.info(f"Payload prepared for caching. Size: {payload_size} bytes.") - except Exception as ser_e: - logger.warning(f"Could not serialize payload for size logging: {ser_e}") - - - logger.info(f"Writing cache file to: {cache_path}") - with open(cache_path, 'w', encoding='utf-8') as f: - json.dump(payload.model_dump(), f, indent=2) - logger.info(f"Wiki cache successfully saved to {cache_path}") - return True - except IOError as e: - logger.error(f"IOError saving wiki cache to {cache_path}: {e.strerror} (errno: {e.errno})", exc_info=True) - return False - except Exception as e: - logger.error(f"Unexpected error saving wiki cache to {cache_path}: {e}", exc_info=True) - return False - -# --- Wiki Cache API Endpoints --- - -@app.get("/api/wiki_cache", response_model=Optional[WikiCacheData]) -async def get_cached_wiki( - owner: str = Query(..., description="Repository owner"), - repo: str = Query(..., description="Repository name"), - repo_type: str = Query(..., description="Repository type (e.g., github, gitlab)"), - language: str = Query(..., description="Language of the wiki content") -): - """ - Retrieves cached wiki data (structure and generated pages) for a repository. - """ - # Language validation - supported_langs = configs["lang_config"]["supported_languages"] - if not supported_langs.__contains__(language): - language = configs["lang_config"]["default"] - - logger.info(f"Attempting to retrieve wiki cache for {owner}/{repo} ({repo_type}), lang: {language}") - cached_data = await read_wiki_cache(owner, repo, repo_type, language) - if cached_data: - return cached_data - else: - # Return 200 with null body if not found, as frontend expects this behavior - # Or, raise HTTPException(status_code=404, detail="Wiki cache not found") if preferred - logger.info(f"Wiki cache not found for {owner}/{repo} ({repo_type}), lang: {language}") - return None - -@app.post("/api/wiki_cache") -async def store_wiki_cache(request_data: WikiCacheRequest): - """ - Stores generated wiki data (structure and pages) to the server-side cache. - """ - # Language validation - supported_langs = configs["lang_config"]["supported_languages"] - - if not supported_langs.__contains__(request_data.language): - request_data.language = configs["lang_config"]["default"] - - logger.info(f"Attempting to save wiki cache for {request_data.repo.owner}/{request_data.repo.repo} ({request_data.repo.type}), lang: {request_data.language}") - success = await save_wiki_cache(request_data) - if success: - return {"message": "Wiki cache saved successfully"} - else: - raise HTTPException(status_code=500, detail="Failed to save wiki cache") - -@app.delete("/api/wiki_cache") -async def delete_wiki_cache( - owner: str = Query(..., description="Repository owner"), - repo: str = Query(..., description="Repository name"), - repo_type: str = Query(..., description="Repository type (e.g., github, gitlab)"), - language: str = Query(..., description="Language of the wiki content"), - authorization_code: Optional[str] = Query(None, description="Authorization code") -): - """ - Deletes a specific wiki cache from the file system. - """ - # Language validation - supported_langs = configs["lang_config"]["supported_languages"] - if not supported_langs.__contains__(language): - raise HTTPException(status_code=400, detail="Language is not supported") - - if WIKI_AUTH_MODE: - logger.info("check the authorization code") - if not authorization_code or WIKI_AUTH_CODE != authorization_code: - raise HTTPException(status_code=401, detail="Authorization code is invalid") - - logger.info(f"Attempting to delete wiki cache for {owner}/{repo} ({repo_type}), lang: {language}") - cache_path = get_wiki_cache_path(owner, repo, repo_type, language) - - if os.path.exists(cache_path): - try: - os.remove(cache_path) - logger.info(f"Successfully deleted wiki cache: {cache_path}") - return {"message": f"Wiki cache for {owner}/{repo} ({language}) deleted successfully"} - except Exception as e: - logger.error(f"Error deleting wiki cache {cache_path}: {e}") - raise HTTPException(status_code=500, detail=f"Failed to delete wiki cache: {str(e)}") - else: - logger.warning(f"Wiki cache not found, cannot delete: {cache_path}") - raise HTTPException(status_code=404, detail="Wiki cache not found") - -@app.get("/health") -async def health_check(): - """Health check endpoint for Docker and monitoring""" - return { - "status": "healthy", - "timestamp": datetime.now().isoformat(), - "service": "deepwiki-api" - } - -@app.get("/") -async def root(): - """Root endpoint to check if the API is running and list available endpoints dynamically.""" - # Collect routes dynamically from the FastAPI app - endpoints = {} - for route in app.routes: - if hasattr(route, "methods") and hasattr(route, "path"): - # Skip docs and static routes - if route.path in ["/openapi.json", "/docs", "/redoc", "/favicon.ico"]: - continue - # Group endpoints by first path segment - path_parts = route.path.strip("/").split("/") - group = path_parts[0].capitalize() if path_parts[0] else "Root" - method_list = list(route.methods - {"HEAD", "OPTIONS"}) - for method in method_list: - endpoints.setdefault(group, []).append(f"{method} {route.path}") - - # Optionally, sort endpoints for readability - for group in endpoints: - endpoints[group].sort() - - return { - "message": "Welcome to Streaming API", - "version": "1.0.0", - "endpoints": endpoints - } - -# --- Processed Projects Endpoint --- (New Endpoint) -@app.get("/api/processed_projects", response_model=List[ProcessedProjectEntry]) -async def get_processed_projects(): - """ - Lists all processed projects found in the wiki cache directory. - Projects are identified by files named like: deepwiki_cache_{repo_type}_{owner}_{repo}_{language}.json - """ - project_entries: List[ProcessedProjectEntry] = [] - # WIKI_CACHE_DIR is already defined globally in the file - - try: - if not os.path.exists(WIKI_CACHE_DIR): - logger.info(f"Cache directory {WIKI_CACHE_DIR} not found. Returning empty list.") - return [] - - logger.info(f"Scanning for project cache files in: {WIKI_CACHE_DIR}") - filenames = await asyncio.to_thread(os.listdir, WIKI_CACHE_DIR) # Use asyncio.to_thread for os.listdir - - for filename in filenames: - if filename.startswith("deepwiki_cache_") and filename.endswith(".json"): - file_path = os.path.join(WIKI_CACHE_DIR, filename) - try: - stats = await asyncio.to_thread(os.stat, file_path) # Use asyncio.to_thread for os.stat - parts = filename.replace("deepwiki_cache_", "").replace(".json", "").split('_') - - # Expecting repo_type_owner_repo_language - # Example: deepwiki_cache_github_AsyncFuncAI_deepwiki-open_en.json - # parts = [github, AsyncFuncAI, deepwiki-open, en] - if len(parts) >= 4: - repo_type = parts[0] - owner = parts[1] - language = parts[-1] # language is the last part - repo = "_".join(parts[2:-1]) # repo can contain underscores - - project_entries.append( - ProcessedProjectEntry( - id=filename, - owner=owner, - repo=repo, - name=f"{owner}/{repo}", - repo_type=repo_type, - submittedAt=int(stats.st_mtime * 1000), # Convert to milliseconds - language=language - ) - ) - else: - logger.warning(f"Could not parse project details from filename: {filename}") - except Exception as e: - logger.error(f"Error processing file {file_path}: {e}") - continue # Skip this file on error - - # Sort by most recent first - project_entries.sort(key=lambda p: p.submittedAt, reverse=True) - logger.info(f"Found {len(project_entries)} processed project entries.") - return project_entries - - except Exception as e: - logger.error(f"Error listing processed projects from {WIKI_CACHE_DIR}: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Failed to list processed projects from server cache.") diff --git a/api/chat/__init__.py b/api/chat/__init__.py index 57008ffc1..b95caee4e 100644 --- a/api/chat/__init__.py +++ b/api/chat/__init__.py @@ -1,11 +1,12 @@ -from api.chat._stream import ChatStreamer from api.chat._prompts import prompt_builder +from api.chat._stream import ChatStreamer def is_token_limit_error(exc: Exception) -> bool: error_message = str(exc).lower() return any( - k in error_message for k in ( + k in error_message + for k in ( "maximum context length", "token limit", "too many tokens", diff --git a/api/chat/_prompts.py b/api/chat/_prompts.py index bef5254d6..a95c9cecf 100644 --- a/api/chat/_prompts.py +++ b/api/chat/_prompts.py @@ -1,25 +1,25 @@ -from api.logging_config import setup_logging -from logging import getLogger +from api.logger import get_logger -setup_logging() -logger = getLogger(__name__) +logger = get_logger("chat") def prompt_builder( - system_prompt: str, - query: str, - conversation_history: str | None = None, - file_path: str | None = None, - file_content: str | None = None, - context: str = "", - simplify: bool = False, + system_prompt: str, + query: str, + conversation_history: str | None = None, + file_path: str | None = None, + file_content: str | None = None, + context: str = "", + simplify: bool = False, ) -> str: prompt = f"/no_think {system_prompt}\n\n" if conversation_history: - prompt += f"\n{conversation_history}\n\n" + prompt += ( + f"\n{conversation_history}\n\n" + ) if file_path and file_content: - prompt += f"\n{file_content}\n\n\n" + prompt += f'\n{file_content}\n\n\n' if not simplify: if context.strip(): @@ -27,7 +27,9 @@ def prompt_builder( else: # Add a note that we're skipping RAG due to size constraints or because it's the isolated API logger.info("No context available from RAG") - context_prompt = "Answering without retrieval augmentation.\n\n" + context_prompt = ( + "Answering without retrieval augmentation.\n\n" + ) else: context_prompt = "Answering without retrieval augmentation due to input size constraints.\n\n" diff --git a/api/chat/_stream.py b/api/chat/_stream.py index 14525769f..dcb7f52ab 100644 --- a/api/chat/_stream.py +++ b/api/chat/_stream.py @@ -1,32 +1,29 @@ -import logging -from abc import abstractmethod, ABC - -from typing import TYPE_CHECKING - +from abc import ABC, abstractmethod from collections.abc import AsyncIterator +from typing import TYPE_CHECKING from adalflow.core.types import ModelType + from api.config import ( - OPENROUTER_API_KEY, - OPENAI_API_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, + GOOGLE_API_KEY, LITELLM_API_KEY, + OPENAI_API_KEY, + OPENROUTER_API_KEY, ) +from api.logger import get_logger if TYPE_CHECKING: from ollama import ChatResponse - from openai.types.chat import ChatCompletionChunk from openai import AsyncStream + from openai.types.chat import ChatCompletionChunk + from api.clients import OpenAIClient MODEL_CFG = dict[str, str | int | float] -# Configure logging -from api.logging_config import setup_logging - -setup_logging() -logger = logging.getLogger(__name__) +logger = get_logger("chat") class ChatStreamer(ABC): @@ -40,7 +37,9 @@ def __init_subclass__(cls, **kwargs) -> None: ChatStreamer._registry[provider] = cls @classmethod - def create(cls, *, provider: str, model: str | None = None, model_config: MODEL_CFG) -> "ChatStreamer": + def create( + cls, *, provider: str, model: str | None = None, model_config: MODEL_CFG + ) -> "ChatStreamer": model = model or model_config.get("model") logger.info("Using %s with model: %s", provider, model) registered = ChatStreamer._registry.get(provider, None) @@ -50,7 +49,9 @@ def create(cls, *, provider: str, model: str | None = None, model_config: MODEL_ @abstractmethod def respond_stream(self, prompt: str) -> AsyncIterator[str]: - raise NotImplementedError(f"{type(self).__name__} does not implement `respond_stream`") + raise NotImplementedError( + f"{type(self).__name__} does not implement `respond_stream`" + ) class OllamaChatStreamer(ChatStreamer): @@ -66,15 +67,16 @@ def __init__(self, *, model: str, model_config: MODEL_CFG): "options": { "temperature": model_config["temperature"], "top_p": model_config["top_p"], - "num_ctx": model_config["num_ctx"] - } + "num_ctx": model_config["num_ctx"], + }, } logger.debug(f"Prompting Ollama with kwargs: {self.model_kwargs}") async def respond_stream(self, prompt: str) -> AsyncIterator[str]: api_kwargs = self.client.convert_inputs_to_api_kwargs( - input=prompt + " /no_think", # todo I think this could be added into model kwargs? + input=prompt + + " /no_think", # todo I think this could be added into model kwargs? model_kwargs=self.model_kwargs, model_type=ModelType.LLM, ) @@ -91,7 +93,7 @@ async def respond_stream(self, prompt: str) -> AsyncIterator[str]: ) text = chunk.message.content if text: - text = text.replace('', '').replace('', '') + text = text.replace("", "").replace("", "") yield text @@ -104,7 +106,9 @@ class OpenRouterChatStreamer(ChatStreamer): def __init__(self, *, model: str, model_config: MODEL_CFG): if not OPENROUTER_API_KEY: - logger.warning("OPENROUTER_API_KEY not configured, but continuing with request") + logger.warning( + "OPENROUTER_API_KEY not configured, but continuing with request" + ) # We'll let the OpenRouterClient handle this and return a friendly error message from api.clients import OpenRouterClient @@ -112,7 +116,7 @@ def __init__(self, *, model: str, model_config: MODEL_CFG): self.model_kwargs = { "model": model, "stream": True, - "temperature": model_config["temperature"] + "temperature": model_config["temperature"], } if "top_k" in model_config: self.model_kwargs["top_k"] = model_config["top_k"] @@ -124,8 +128,8 @@ async def respond_stream(self, prompt: str) -> AsyncIterator[str]: model_type=ModelType.LLM, ) async for chunk in await self.client.acall( - api_kwargs=api_kwargs, - model_type=ModelType.LLM, + api_kwargs=api_kwargs, + model_type=ModelType.LLM, ): yield chunk @@ -139,7 +143,7 @@ def __init__(self, *, model: str, model_config: MODEL_CFG): self.model_kwargs = { "model": model, "stream": True, - "temperature": model_config["temperature"] + "temperature": model_config["temperature"], } # Only add top_p if it exists in the model config if "top_p" in model_config: @@ -153,9 +157,7 @@ def _build_client(self) -> "OpenAIClient": async def respond_stream(self, prompt: str) -> AsyncIterator[str]: api_kwargs = self.client.convert_inputs_to_api_kwargs( - input=prompt, - model_kwargs=self.model_kwargs, - model_type=ModelType.LLM + input=prompt, model_kwargs=self.model_kwargs, model_type=ModelType.LLM ) response: "AsyncStream[ChatCompletionChunk]" = await self.client.acall( api_kwargs=api_kwargs, @@ -164,9 +166,9 @@ async def respond_stream(self, prompt: str) -> AsyncIterator[str]: async for chunk in response: if ( - chunk.choices and - chunk.choices[0].delta is not None and - chunk.choices[0].delta.content is not None + chunk.choices + and chunk.choices[0].delta is not None + and chunk.choices[0].delta.content is not None ): yield chunk.choices[0].delta.content @@ -186,6 +188,7 @@ def __init__(self, *, model: str, model_config: MODEL_CFG): def _build_client(self): from api.clients import OpenAIClient + return OpenAIClient() @@ -199,6 +202,7 @@ class AzureChatStreamer(_OpenAICompatStreamer): def _build_client(self): from api.clients import AzureAIClient + return AzureAIClient() @@ -211,13 +215,16 @@ class LiteLLMChatStreamer(_OpenAICompatStreamer): def __init__(self, *, model: str, model_config: MODEL_CFG): if not LITELLM_API_KEY: - logger.warning("LITELLM_API_KEY not configured, but continuing with request") + logger.warning( + "LITELLM_API_KEY not configured, but continuing with request" + ) # We'll let the OpenAIClient handle this and return an error message super().__init__(model=model, model_config=model_config) def _build_client(self): from api.clients import LiteLLMClient + return LiteLLMClient() @@ -230,7 +237,9 @@ class BedrockChatStreamer(ChatStreamer): def __init__(self, *, model: str, model_config: MODEL_CFG): if not AWS_ACCESS_KEY_ID or not AWS_SECRET_ACCESS_KEY: - logger.warning("AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY not configured, but continuing with request") + logger.warning( + "AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY not configured, but continuing with request" + ) # We'll let the BedrockClient handle this and return an error message from api.clients import BedrockClient @@ -238,8 +247,8 @@ def __init__(self, *, model: str, model_config: MODEL_CFG): self.model_kwargs = {"model": model} for key in ( - "temperature", - "top_p", + "temperature", + "top_p", ): if key in model_config: self.model_kwargs[key] = model_config[key] @@ -299,13 +308,15 @@ def __init__(self, *, model: str, model_config: MODEL_CFG): import google.generativeai as genai from google.generativeai.types import GenerationConfig + genai.configure(api_key=GOOGLE_API_KEY) + self.client = genai.GenerativeModel( model_name=model, generation_config=GenerationConfig( temperature=model_config.get("temperature"), top_p=model_config.get("top_p"), top_k=model_config.get("top_k"), - ) + ), ) async def respond_stream(self, prompt: str) -> AsyncIterator[str]: @@ -313,3 +324,51 @@ async def respond_stream(self, prompt: str) -> AsyncIterator[str]: async for chunk in response: if hasattr(chunk, "text"): yield chunk.text + + +class AnthropicChatStreamer(ChatStreamer): + provider = "anthropic" + + def __init__(self, *, model: str, model_config: MODEL_CFG): + from api.config import ( + AWS_ACCESS_KEY_ID, + AWS_REGION, + AWS_SECRET_ACCESS_KEY, + AWS_SESSION_TOKEN, + ) + + from ..clients.anthropic import AnthropicBedrockClient + + self.client = AnthropicBedrockClient( + aws_access_key_id=AWS_ACCESS_KEY_ID, + aws_session_token=AWS_SESSION_TOKEN, + aws_secret_access_key=AWS_SECRET_ACCESS_KEY, + aws_region=AWS_REGION, + ) + + self.model_kwargs = { + "model": model, + "stream": True, + "max_tokens": model_config["max_tokens"], # max_tokens must exist + } + for key in ( + "temperature", + "top_p", + ): + if key in model_config: + self.model_kwargs[key] = model_config[key] + + async def respond_stream(self, prompt: str) -> AsyncIterator[str]: + api_kwargs = self.client.convert_inputs_to_api_kwargs( + input=prompt, + model_kwargs=self.model_kwargs, + model_type=ModelType.LLM, + ) + + response = await self.client.acall( + api_kwargs=api_kwargs, + model_type=ModelType.LLM, + ) + async for chunk in response: + if chunk.type == "content_block_delta" and chunk.delta.type == "text_delta": + yield chunk.delta.text diff --git a/api/chat_model.py b/api/chat_model.py deleted file mode 100644 index 4af8a03d8..000000000 --- a/api/chat_model.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import Optional, List, Literal -from urllib.parse import unquote - -from pydantic import BaseModel, Field, field_validator - -# Models for the API -class ChatMessage(BaseModel): - role: str # 'user' or 'assistant' - content: str - mode: Literal["normal", "deep_research"] = Field(default="normal") - -class ChatCompletionRequest(BaseModel): - """ - Model for requesting a chat completion. - """ - repo_url: str = Field(..., description="URL of the repository to query") - messages: List[ChatMessage] = Field(..., description="List of chat messages") - filePath: Optional[str] = Field(None, description="Optional path to a file in the repository to include in the prompt") - token: Optional[str] = Field(None, description="Personal access token for private repositories") - type: Optional[Literal["local", "github", "gitlab", "bitbucket"]] = Field( - "github", - description="Type of repository (e.g., 'local', 'github', 'gitlab', 'bitbucket')", - ) - - # model parameters - provider: str = Field("google", description="Model provider (google, openai, openrouter, ollama, bedrock, azure, dashscope)") - model: Optional[str] = Field(None, description="Model name for the specified provider") - - language: Optional[str] = Field("en", description="Language for content generation (e.g., 'en', 'ja', 'zh', 'es', 'kr', 'vi')") - - research_iteration: int = Field( - default=1, - ge=1, - description="Current deep research iteration (1-based). Only used when the request is in deep_research mode.", - ) - - excluded_dirs: List[str] = Field( - default_factory=list, - description="List or newline-separated string of directories to exclude from processing", - ) - excluded_files: List[str] = Field( - default_factory=list, - description="List or newline-separated string of file patterns to exclude from processing", - ) - included_dirs: List[str] = Field( - default_factory=list, - description="List or newline-separated string of directories to include exclusively", - ) - included_files: List[str] = Field( - default_factory=list, - description="List or newline-separated string of file patterns to include exclusively", - ) - - @field_validator( - "excluded_dirs", - "excluded_files", - "included_dirs", - "included_files", - mode="before", - ) - @classmethod - def validate_path(cls, value: list[str] | str) -> list[str]: - if isinstance(value, str): - value = [unquote(path) for path in value.strip().split("\n") if path] - return value diff --git a/api/clients/__init__.py b/api/clients/__init__.py index 59661eba1..d68b77cea 100644 --- a/api/clients/__init__.py +++ b/api/clients/__init__.py @@ -4,9 +4,11 @@ from adalflow.components.model_client import ( AzureAIClient, - OpenAIClient, GoogleGenAIClient, + OpenAIClient, ) + +from .anthropic import AnthropicBedrockClient from .bedrock import BedrockClient from .dashscope import DashscopeClient from .google_embedder import GoogleEmbedderClient @@ -15,6 +17,7 @@ from .openrouter import OpenRouterClient __all__ = [ + "AnthropicBedrockClient", "AzureAIClient", "BedrockClient", "DashscopeClient", diff --git a/api/clients/anthropic.py b/api/clients/anthropic.py new file mode 100644 index 000000000..45eb366b8 --- /dev/null +++ b/api/clients/anthropic.py @@ -0,0 +1,183 @@ +from typing import Any + +import backoff +from adalflow.core.model_client import ModelClient +from adalflow.core.types import ModelType +from anthropic import ( + APITimeoutError, + BadRequestError, + InternalServerError, + RateLimitError, + UnprocessableEntityError, +) + + +class AnthropicBedrockClient(ModelClient): + def __init__( + self, + aws_access_key_id: str | None = None, + aws_secret_access_key: str | None = None, + aws_session_token: str | None = None, + aws_region: str | None = None, + **kwargs, + ): + """A client wrapper for interacting with Anthropic Bedrock API. + + This class currently only provides chat completion API calls. + + Parameters + ---------- + aws_access_key_id: str, optional. + AWS access key ID. Defaults to None. + aws_secret_access_key: str, optional. + AWS secret access key. Defaults to None. + aws_session_token: str, optional. + AWS session token. Defaults to None. + aws_region: str, optional. + AWS region. Defaults to None. + + Examples + -------- + .. code-block:: python + + from api.clients import AnthropicBedrockClient + from adalflow.core.types import ModelType + client = AnthropicBedrockClient() + + # chat completion API + api_kwargs = client.convert_inputs_to_api_kwargs( + inputs="Hello World!", + model_kwargs={"max_tokens": 2048}, + model_type=ModelType.LLM, + ) + + # synchronous API call + response = client.call(**api_kwargs, model_type=ModelType.LLM) + + # asynchronous API call + response = await client.acall(**api_kwargs, model_type=ModelType.LLM) + + References + ---------- + - [AWS Bedrock API Documentation](https://platform.claude.com/docs/zh-TW/build-with-claude/claude-in-amazon-bedrock#making-your-first-request) + + """ + super().__init__() + self._aws_client_kwargs = dict( + aws_access_key=aws_access_key_id, + aws_secret_key=aws_secret_access_key, + aws_session_token=aws_session_token, + aws_region=aws_region, + ) + + def init_sync_client(self): + from anthropic import AnthropicBedrock + + return AnthropicBedrock(**self._aws_client_kwargs) + + def init_async_client(self): + from anthropic import AsyncAnthropicBedrock + + return AsyncAnthropicBedrock(**self._aws_client_kwargs) + + @property + def async_client(self): + if getattr(self, "_async_client", None) is None: + self._async_client = self.init_async_client() + return self._async_client + + @async_client.setter + def async_client(self, value): + from anthropic import AsyncAnthropicBedrock + + if value is None and isinstance( + getattr(self, "_async_client", None), AsyncAnthropicBedrock + ): + self.async_client.close() + self._async_client = value + + @property + def sync_client(self): + if getattr(self, "_sync_client", None) is None: + self._sync_client = self.init_sync_client() + return self._sync_client + + @sync_client.setter + def sync_client(self, value): + from anthropic import AnthropicBedrock + + if value is None and isinstance( + getattr(self, "_sync_client", None), AnthropicBedrock + ): + self.sync_client.close() + self._sync_client = value + + def convert_inputs_to_api_kwargs( + self, + input: Any = None, + model_kwargs: dict | None = None, + model_type: ModelType = ModelType.UNDEFINED, + ) -> dict[str, Any]: + final_model_kwargs = model_kwargs.copy() if model_kwargs else {} + if model_type == ModelType.LLM: + if isinstance(input, str): + input = [{"role": "user", "content": input}] + elif not isinstance(input, list): + raise ValueError( + f"input must be a string or a list or messages, get {type(input).__name__}" + ) + final_model_kwargs["messages"] = input + return final_model_kwargs + + raise ValueError(f"model_type {model_type} is not supported") + + @backoff.on_exception( + backoff.expo, + ( + APITimeoutError, + InternalServerError, + RateLimitError, + ), + max_time=5, + ) + def call( + self, api_kwargs: dict | None = None, model_type: ModelType | None = None + ) -> Any: + api_kwargs = api_kwargs or {} + if model_type != ModelType.LLM: + raise ValueError(f"model_type {model_type} is not supported") + + if "model" not in api_kwargs: + raise ValueError("must provide 'model' parameter in api_kwargs") + + return self.sync_client.messages.create(**api_kwargs) + + @backoff.on_exception( + backoff.expo, + ( + APITimeoutError, + InternalServerError, + RateLimitError, + UnprocessableEntityError, + BadRequestError, + ), + max_time=5, + ) + async def acall( + self, api_kwargs: dict | None = None, model_type: ModelType | None = None + ) -> Any: + api_kwargs = api_kwargs or {} + if model_type != ModelType.LLM: + raise ValueError(f"model_type {model_type} is not supported") + + if "model" not in api_kwargs: + raise ValueError("must provide 'model' parameter in api_kwargs") + + return await self.async_client.messages.create(**api_kwargs) + + def to_dict(self, exclude: list[str] | None = None) -> dict[str, Any]: + return self._aws_client_kwargs + + @classmethod + def from_dict(cls, data: dict[str, Any]): + return cls(**data) diff --git a/api/clients/bedrock.py b/api/clients/bedrock.py index fa0b5097b..e74659eb1 100644 --- a/api/clients/bedrock.py +++ b/api/clients/bedrock.py @@ -1,21 +1,19 @@ """AWS Bedrock ModelClient integration.""" -import os import json -import logging +from typing import Any, Dict, List, Optional, Sequence + +import backoff import boto3 import botocore -import backoff -from typing import Dict, Any, Optional, List, Generator, Union, AsyncGenerator, Sequence - from adalflow.core.model_client import ModelClient -from adalflow.core.types import ModelType, GeneratorOutput, EmbedderOutput +from adalflow.core.types import EmbedderOutput, ModelType # Configure logging -from api.logging_config import setup_logging +from api.logger import get_logger + +log = get_logger(__name__) -setup_logging() -log = logging.getLogger(__name__) class BedrockClient(ModelClient): __doc__ = r"""A component wrapper for the AWS Bedrock API client. @@ -43,10 +41,10 @@ def __init__( aws_region: Optional[str] = None, aws_role_arn: Optional[str] = None, *args, - **kwargs + **kwargs, ) -> None: """Initialize the AWS Bedrock client. - + Args: aws_access_key_id: AWS access key ID. If not provided, will use environment variable AWS_ACCESS_KEY_ID. aws_secret_access_key: AWS secret access key. If not provided, will use environment variable AWS_SECRET_ACCESS_KEY. @@ -57,10 +55,10 @@ def __init__( super().__init__(*args, **kwargs) from api.config import ( AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY, - AWS_SESSION_TOKEN, AWS_REGION, AWS_ROLE_ARN, + AWS_SECRET_ACCESS_KEY, + AWS_SESSION_TOKEN, ) self.aws_access_key_id = aws_access_key_id or AWS_ACCESS_KEY_ID @@ -68,7 +66,7 @@ def __init__( self.aws_session_token = aws_session_token or AWS_SESSION_TOKEN self.aws_region = aws_region or AWS_REGION or "us-east-1" self.aws_role_arn = aws_role_arn or AWS_ROLE_ARN - + self.sync_client = self.init_sync_client() self.async_client = None # Initialize async client only when needed @@ -94,10 +92,10 @@ def __getstate__(self): """ state = self.__dict__.copy() # Remove the unpicklable client instances - if 'sync_client' in state: - del state['sync_client'] - if 'async_client' in state: - del state['async_client'] + if "sync_client" in state: + del state["sync_client"] + if "async_client" in state: + del state["async_client"] return state def __setstate__(self, state): @@ -118,34 +116,32 @@ def init_sync_client(self): aws_access_key_id=self.aws_access_key_id, aws_secret_access_key=self.aws_secret_access_key, aws_session_token=self.aws_session_token, - region_name=self.aws_region + region_name=self.aws_region, ) - + # If a role ARN is provided, assume that role if self.aws_role_arn: - sts_client = session.client('sts') + sts_client = session.client("sts") assumed_role = sts_client.assume_role( - RoleArn=self.aws_role_arn, - RoleSessionName="DeepWikiBedrockSession" + RoleArn=self.aws_role_arn, RoleSessionName="DeepWikiBedrockSession" ) - credentials = assumed_role['Credentials'] - + credentials = assumed_role["Credentials"] + # Create a new session with the assumed role credentials session = boto3.Session( - aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=self.aws_region + aws_access_key_id=credentials["AccessKeyId"], + aws_secret_access_key=credentials["SecretAccessKey"], + aws_session_token=credentials["SessionToken"], + region_name=self.aws_region, ) - + # Create the Bedrock client bedrock_runtime = session.client( - service_name='bedrock-runtime', - region_name=self.aws_region + service_name="bedrock-runtime", region_name=self.aws_region ) - + return bedrock_runtime - + except Exception as e: log.error(f"Error initializing AWS Bedrock client: {str(e)}") # Return None to indicate initialization failure @@ -153,7 +149,7 @@ def init_sync_client(self): def init_async_client(self): """Initialize the asynchronous AWS Bedrock client. - + Note: boto3 doesn't have native async support, so we'll use the sync client in async methods and handle async behavior at a higher level. """ @@ -162,10 +158,10 @@ def init_async_client(self): def _get_model_provider(self, model_id: str) -> str: """Extract the provider from the model ID. - + Args: model_id: The model inference ID, e.g., "anthropic.claude-3-sonnet-20240229-v1:0", "global.anthropic.claude-sonnet-4-5-20250929-v1:0", or "global.cohere.embed-v4:0" - + Returns: The provider name, e.g., "anthropic" """ @@ -180,14 +176,16 @@ def _get_model_provider(self, model_id: str) -> str: # Default to Amazon if format is unexpected return "amazon" - def _format_prompt_for_provider(self, provider: str, prompt: str, messages=None) -> Dict[str, Any]: + def _format_prompt_for_provider( + self, provider: str, prompt: str, messages=None + ) -> Dict[str, Any]: """Format the prompt according to the provider's requirements. - + Args: provider: The provider name, e.g., "anthropic" prompt: The prompt text messages: Optional list of messages for chat models - + Returns: A dictionary with the formatted prompt """ @@ -198,14 +196,18 @@ def _format_prompt_for_provider(self, provider: str, prompt: str, messages=None) formatted_messages = [] for msg in messages: role = "user" if msg.get("role") == "user" else "assistant" - formatted_messages.append({ - "role": role, - "content": [{"type": "text", "text": msg.get("content", "")}] - }) + formatted_messages.append( + { + "role": role, + "content": [ + {"type": "text", "text": msg.get("content", "")} + ], + } + ) return { "anthropic_version": "bedrock-2023-05-31", "messages": formatted_messages, - "max_tokens": 4096 + "max_tokens": 4096, } else: # Format as a single prompt @@ -214,7 +216,7 @@ def _format_prompt_for_provider(self, provider: str, prompt: str, messages=None) "messages": [ {"role": "user", "content": [{"type": "text", "text": prompt}]} ], - "max_tokens": 4096 + "max_tokens": 4096, } elif provider == "amazon": # Format for Amazon Titan models @@ -224,24 +226,19 @@ def _format_prompt_for_provider(self, provider: str, prompt: str, messages=None) "maxTokenCount": 4096, "stopSequences": [], "temperature": 0.7, - "topP": 0.8 - } + "topP": 0.8, + }, } elif provider == "cohere": # Format for Cohere models - return { - "prompt": prompt, - "max_tokens": 4096, - "temperature": 0.7, - "p": 0.8 - } + return {"prompt": prompt, "max_tokens": 4096, "temperature": 0.7, "p": 0.8} elif provider == "ai21": # Format for AI21 models return { "prompt": prompt, "maxTokens": 4096, "temperature": 0.7, - "topP": 0.8 + "topP": 0.8, } else: # Default format @@ -249,11 +246,11 @@ def _format_prompt_for_provider(self, provider: str, prompt: str, messages=None) def _extract_response_text(self, provider: str, response: Dict[str, Any]) -> str: """Extract the generated text from the response. - + Args: provider: The provider name, e.g., "anthropic" response: The response from the Bedrock API - + Returns: The generated text """ @@ -283,15 +280,20 @@ def parse_embedding_response(self, response: Any) -> EmbedderOutput: if isinstance(response, dict) and "embeddings" in response: embeddings = response.get("embeddings") or [] embedding_data = [ - Embedding(embedding=emb, index=i) for i, emb in enumerate(embeddings) + Embedding(embedding=emb, index=i) + for i, emb in enumerate(embeddings) ] elif isinstance(response, dict) and "embedding" in response: emb = response.get("embedding") or [] embedding_data = [Embedding(embedding=emb, index=0)] else: - raise ValueError(f"Unexpected embedding response type: {type(response)}") + raise ValueError( + f"Unexpected embedding response type: {type(response)}" + ) - return EmbedderOutput(data=embedding_data, error=None, raw_response=response) + return EmbedderOutput( + data=embedding_data, error=None, raw_response=response + ) except Exception as e: log.error(f"Error parsing Bedrock embedding response: {e}") return EmbedderOutput(data=[], error=str(e), raw_response=response) @@ -304,35 +306,39 @@ def parse_embedding_response(self, response: Any) -> EmbedderOutput: def call(self, api_kwargs: Dict = None, model_type: ModelType = None) -> Any: """Make a synchronous call to the AWS Bedrock API.""" api_kwargs = api_kwargs or {} - + # Check if client is initialized if not self.sync_client: error_msg = "AWS Bedrock client not initialized. Check your AWS credentials and region." log.error(error_msg) return error_msg - + if model_type == ModelType.LLM: - model_id = api_kwargs.get("model", "anthropic.claude-3-sonnet-20240229-v1:0") + model_id = api_kwargs.get( + "model", "anthropic.claude-3-sonnet-20240229-v1:0" + ) provider = self._get_model_provider(model_id) - + # Get the prompt from api_kwargs prompt = api_kwargs.get("input", "") messages = api_kwargs.get("messages") - + # Format the prompt according to the provider request_body = self._format_prompt_for_provider(provider, prompt, messages) - + # Add model parameters if provided if "temperature" in api_kwargs: if provider == "anthropic": request_body["temperature"] = api_kwargs["temperature"] elif provider == "amazon": - request_body["textGenerationConfig"]["temperature"] = api_kwargs["temperature"] + request_body["textGenerationConfig"]["temperature"] = api_kwargs[ + "temperature" + ] elif provider == "cohere": request_body["temperature"] = api_kwargs["temperature"] elif provider == "ai21": request_body["temperature"] = api_kwargs["temperature"] - + if "top_p" in api_kwargs: if provider == "anthropic": request_body["top_p"] = api_kwargs["top_p"] @@ -342,25 +348,22 @@ def call(self, api_kwargs: Dict = None, model_type: ModelType = None) -> Any: request_body["p"] = api_kwargs["top_p"] elif provider == "ai21": request_body["topP"] = api_kwargs["top_p"] - + # Convert request body to JSON body = json.dumps(request_body) - + try: # Make the API call - response = self.sync_client.invoke_model( - modelId=model_id, - body=body - ) - + response = self.sync_client.invoke_model(modelId=model_id, body=body) + # Parse the response response_body = json.loads(response["body"].read()) - + # Extract the generated text generated_text = self._extract_response_text(provider, response_body) - + return generated_text - + except Exception as e: log.error(f"Error calling AWS Bedrock API: {str(e)}") return f"Error: {str(e)}" @@ -400,7 +403,9 @@ def call(self, api_kwargs: Dict = None, model_type: ModelType = None) -> Any: emb = response_body.get("embedding") if emb is None: - raise ValueError(f"Embedding not found in response: {response_body}") + raise ValueError( + f"Embedding not found in response: {response_body}" + ) embeddings.append(emb) elif provider == "cohere": @@ -426,12 +431,18 @@ def call(self, api_kwargs: Dict = None, model_type: ModelType = None) -> Any: elif isinstance(batch_embeddings, dict) and "float" in batch_embeddings: embeddings = batch_embeddings["float"] else: - raise ValueError(f"Embeddings not found in response: {response_body}") + raise ValueError( + f"Embeddings not found in response: {response_body}" + ) else: - raise NotImplementedError(f"Embedding provider '{provider}' is not supported by the Bedrock client.") + raise NotImplementedError( + f"Embedding provider '{provider}' is not supported by the Bedrock client." + ) return {"embeddings": embeddings, "raw_responses": raw_responses} else: - raise ValueError(f"Model type {model_type} is not supported by AWS Bedrock client") + raise ValueError( + f"Model type {model_type} is not supported by AWS Bedrock client" + ) async def acall(self, api_kwargs: Dict = None, model_type: ModelType = None) -> Any: """Make an asynchronous call to the AWS Bedrock API.""" @@ -445,17 +456,19 @@ def convert_inputs_to_api_kwargs( """Convert inputs to API kwargs for AWS Bedrock.""" model_kwargs = model_kwargs or {} api_kwargs = {} - + if model_type == ModelType.LLM: - api_kwargs["model"] = model_kwargs.get("model", "anthropic.claude-3-sonnet-20240229-v1:0") + api_kwargs["model"] = model_kwargs.get( + "model", "anthropic.claude-3-sonnet-20240229-v1:0" + ) api_kwargs["input"] = input - + # Add model parameters if "temperature" in model_kwargs: api_kwargs["temperature"] = model_kwargs["temperature"] if "top_p" in model_kwargs: api_kwargs["top_p"] = model_kwargs["top_p"] - + return api_kwargs elif model_type == ModelType.EMBEDDER: if isinstance(input, str): @@ -465,9 +478,13 @@ def convert_inputs_to_api_kwargs( else: raise TypeError("input must be a string or sequence of strings") - api_kwargs["model"] = model_kwargs.get("model", "amazon.titan-embed-text-v2:0") + api_kwargs["model"] = model_kwargs.get( + "model", "amazon.titan-embed-text-v2:0" + ) api_kwargs["input"] = inputs api_kwargs["model_kwargs"] = model_kwargs return api_kwargs else: - raise ValueError(f"Model type {model_type} is not supported by AWS Bedrock client") + raise ValueError( + f"Model type {model_type} is not supported by AWS Bedrock client" + ) diff --git a/api/clients/dashscope.py b/api/clients/dashscope.py index 5c9174431..a9ec66102 100644 --- a/api/clients/dashscope.py +++ b/api/clients/dashscope.py @@ -2,80 +2,81 @@ import os import pickle +from copy import deepcopy from typing import ( - Dict, - Optional, Any, Callable, + Dict, Generator, - Union, - Literal, List, - Sequence, + Literal, + Optional, + Union, ) -import logging import backoff -from copy import deepcopy + +from adalflow.utils.lazy_import import OptionalPackages, safe_import from tqdm import tqdm -# optional import -from adalflow.utils.lazy_import import safe_import, OptionalPackages +import adalflow.core.functional as F +from adalflow.components.model_client.utils import parse_embedding_response +from adalflow.core.component import DataComponent +from adalflow.core.embedder import ( + BatchEmbedderInputType, + BatchEmbedderOutputType, +) +from adalflow.core.model_client import ModelClient +from adalflow.core.types import ( + CompletionUsage, + Document, + EmbedderInputType, + EmbedderOutput, + EmbedderOutputType, + Embedding, + GeneratorOutput, + ModelType, +) + +# ruff: noqa: E402 openai = safe_import(OptionalPackages.OPENAI.value[0], OptionalPackages.OPENAI.value[1]) +# ruff: noqa: E402 -from openai import OpenAI, AsyncOpenAI, Stream from openai import ( APITimeoutError, + AsyncOpenAI, + BadRequestError, InternalServerError, + OpenAI, RateLimitError, + Stream, UnprocessableEntityError, - BadRequestError, ) from openai.types import ( Completion, CreateEmbeddingResponse, ) -from openai.types.chat import ChatCompletionChunk, ChatCompletion - -from adalflow.core.model_client import ModelClient -from adalflow.core.types import ( - ModelType, - EmbedderOutput, - CompletionUsage, - GeneratorOutput, - Document, - Embedding, - EmbedderOutputType, - EmbedderInputType, -) -from adalflow.core.component import DataComponent -from adalflow.core.embedder import ( - BatchEmbedderOutputType, - BatchEmbedderInputType, -) -import adalflow.core.functional as F -from adalflow.components.model_client.utils import parse_embedding_response +from openai.types.chat import ChatCompletion, ChatCompletionChunk -from api.logging_config import setup_logging +from api.logger import get_logger -# # Disable tqdm progress bars -# os.environ["TQDM_DISABLE"] = "1" +log = get_logger(__name__) -setup_logging() -log = logging.getLogger(__name__) def get_first_message_content(completion: ChatCompletion) -> str: """When we only need the content of the first message.""" log.info(f"🔍 get_first_message_content called with: {type(completion)}") log.debug(f"raw completion: {completion}") - + try: - if hasattr(completion, 'choices') and len(completion.choices) > 0: + if hasattr(completion, "choices") and len(completion.choices) > 0: choice = completion.choices[0] - if hasattr(choice, 'message') and hasattr(choice.message, 'content'): + if hasattr(choice, "message") and hasattr(choice.message, "content"): content = choice.message.content - log.info(f"✅ Successfully extracted content: {type(content)}, length: {len(content) if content else 0}") + log.info( + f"✅ Successfully extracted content: {type(content)}, length: {len(content) if content else 0}" + ) return content else: log.error("❌ Choice doesn't have message.content") @@ -105,7 +106,7 @@ class DashscopeClient(ModelClient): """A component wrapper for the Dashscope (Alibaba Cloud) API client. Dashscope provides access to Alibaba Cloud's Qwen and other models through an OpenAI-compatible API. - + Args: api_key (Optional[str], optional): Dashscope API key. Defaults to None. workspace_id (Optional[str], optional): Dashscope workspace ID. Defaults to None. @@ -134,10 +135,12 @@ def __init__( self._env_api_key_name = env_api_key_name self._env_workspace_id_name = env_workspace_id_name self._env_base_url_name = env_base_url_name - self.base_url = base_url or os.getenv(self._env_base_url_name, "https://dashscope.aliyuncs.com/compatible-mode/v1") + self.base_url = base_url or os.getenv( + self._env_base_url_name, "https://dashscope.aliyuncs.com/compatible-mode/v1" + ) self.sync_client = self.init_sync_client() self.async_client = None - + # Force use of get_first_message_content to ensure string output self.chat_completion_parser = get_first_message_content self._input_type = input_type @@ -146,52 +149,54 @@ def __init__( def _prepare_client_config(self): """ Private helper method to prepare client configuration. - + Returns: tuple: (api_key, workspace_id, base_url) for client initialization - + Raises: ValueError: If API key is not provided """ api_key = self._api_key or os.getenv(self._env_api_key_name) workspace_id = self._workspace_id or os.getenv(self._env_workspace_id_name) - + if not api_key: raise ValueError( f"Environment variable {self._env_api_key_name} must be set" ) - + if not workspace_id: - log.warning(f"Environment variable {self._env_workspace_id_name} not set. Some features may not work properly.") - + log.warning( + f"Environment variable {self._env_workspace_id_name} not set. Some features may not work properly." + ) + # For Dashscope, we need to include the workspace ID in the base URL if provided base_url = self.base_url if workspace_id: # Add workspace ID to headers or URL as required by Dashscope base_url = f"{self.base_url.rstrip('/')}" - + return api_key, workspace_id, base_url def init_sync_client(self): api_key, workspace_id, base_url = self._prepare_client_config() - + client = OpenAI(api_key=api_key, base_url=base_url) - + # Store workspace_id for later use in requests if workspace_id: client._workspace_id = workspace_id - + return client def init_async_client(self): api_key, workspace_id, base_url = self._prepare_client_config() - + client = AsyncOpenAI(api_key=api_key, base_url=base_url) - + # Store workspace_id for later use in requests if workspace_id: client._workspace_id = workspace_id - + return client def parse_chat_completion( @@ -203,17 +208,18 @@ def parse_chat_completion( # If the completion is already a GeneratorOutput, return it directly (prevent recursion) if isinstance(completion, GeneratorOutput): return completion - + # Check if it's a ChatCompletion object (non-streaming response) - if hasattr(completion, 'choices') and hasattr(completion, 'usage'): + if hasattr(completion, "choices") and hasattr(completion, "usage"): # ALWAYS extract the string content directly try: # Direct extraction of message content - if (hasattr(completion, 'choices') and - len(completion.choices) > 0 and - hasattr(completion.choices[0], 'message') and - hasattr(completion.choices[0].message, 'content')): - + if ( + hasattr(completion, "choices") + and len(completion.choices) > 0 + and hasattr(completion.choices[0], "message") + and hasattr(completion.choices[0].message, "content") + ): content = completion.choices[0].message.content if isinstance(content, str): parsed_data = content @@ -222,11 +228,11 @@ def parse_chat_completion( else: # Fallback: convert entire completion to string parsed_data = str(completion) - - except Exception as e: + + except Exception: # Ultimate fallback parsed_data = str(completion) - + return GeneratorOutput( data=parsed_data, usage=CompletionUsage( @@ -244,12 +250,12 @@ def parse_chat_completion( if chunk.choices[0].delta.content: content_parts.append(chunk.choices[0].delta.content) # Try to get usage info from the last chunk - if hasattr(chunk, 'usage') and chunk.usage: + if hasattr(chunk, "usage") and chunk.usage: usage_info = chunk.usage - + # Join all content parts into a single string - full_content = ''.join(content_parts) - + full_content = "".join(content_parts) + # Create usage object usage = None if usage_info: @@ -258,11 +264,9 @@ def parse_chat_completion( prompt_tokens=usage_info.prompt_tokens, total_tokens=usage_info.total_tokens, ) - + return GeneratorOutput( - data=full_content, - usage=usage, - raw_response="streaming" + data=full_content, usage=usage, raw_response="streaming" ) except Exception as e: log.error(f"Error parsing completion: {e}") @@ -293,9 +297,11 @@ def parse_embedding_response( if result.data: log.info(f"🔍 Number of embeddings: {len(result.data)}") if len(result.data) > 0: - log.info(f"🔍 First embedding length: {len(result.data[0].embedding) if hasattr(result.data[0], 'embedding') else 'N/A'}") + log.info( + f"🔍 First embedding length: {len(result.data[0].embedding) if hasattr(result.data[0], 'embedding') else 'N/A'}" + ) else: - log.warning(f"🔍 No embedding data found in result") + log.warning("🔍 No embedding data found in result") return result except Exception as e: log.error(f"🔍 Error parsing DashScope embedding response: {e}") @@ -310,7 +316,7 @@ def convert_inputs_to_api_kwargs( ) -> Dict: """Convert inputs to API kwargs.""" final_model_kwargs = model_kwargs.copy() - + if model_type == ModelType.LLM: messages = [] if isinstance(input, str): @@ -319,22 +325,21 @@ def convert_inputs_to_api_kwargs( messages = input else: raise ValueError(f"Unsupported input type: {type(input)}") - - api_kwargs = { - "messages": messages, - **final_model_kwargs - } - + + api_kwargs = {"messages": messages, **final_model_kwargs} + # Add workspace ID to headers if available - workspace_id = getattr(self.sync_client, '_workspace_id', None) or getattr(self.async_client, '_workspace_id', None) + workspace_id = getattr(self.sync_client, "_workspace_id", None) or getattr( + self.async_client, "_workspace_id", None + ) if workspace_id: # Dashscope may require workspace ID in headers - if 'extra_headers' not in api_kwargs: - api_kwargs['extra_headers'] = {} - api_kwargs['extra_headers']['X-DashScope-WorkSpace'] = workspace_id - + if "extra_headers" not in api_kwargs: + api_kwargs["extra_headers"] = {} + api_kwargs["extra_headers"]["X-DashScope-WorkSpace"] = workspace_id + return api_kwargs - + elif model_type == ModelType.EMBEDDER: # Convert Documents to text strings for embedding processed_input = input @@ -342,7 +347,7 @@ def convert_inputs_to_api_kwargs( # Extract text from Document objects processed_input = [] for item in input: - if hasattr(item, 'text'): + if hasattr(item, "text"): # It's a Document object, extract text processed_input.append(item.text) elif isinstance(item, str): @@ -351,7 +356,7 @@ def convert_inputs_to_api_kwargs( else: # Try to convert to string processed_input.append(str(item)) - elif hasattr(input, 'text'): + elif hasattr(input, "text"): # Single Document object processed_input = input.text elif isinstance(input, str): @@ -360,19 +365,18 @@ def convert_inputs_to_api_kwargs( else: # Convert to string as fallback processed_input = str(input) - - api_kwargs = { - "input": processed_input, - **final_model_kwargs - } - + + api_kwargs = {"input": processed_input, **final_model_kwargs} + # Add workspace ID to headers if available - workspace_id = getattr(self.sync_client, '_workspace_id', None) or getattr(self.async_client, '_workspace_id', None) + workspace_id = getattr(self.sync_client, "_workspace_id", None) or getattr( + self.async_client, "_workspace_id", None + ) if workspace_id: - if 'extra_headers' not in api_kwargs: - api_kwargs['extra_headers'] = {} - api_kwargs['extra_headers']['X-DashScope-WorkSpace'] = workspace_id - + if "extra_headers" not in api_kwargs: + api_kwargs["extra_headers"] = {} + api_kwargs["extra_headers"]["X-DashScope-WorkSpace"] = workspace_id + return api_kwargs else: raise ValueError(f"model_type {model_type} is not supported") @@ -399,7 +403,7 @@ def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINE api_kwargs["extra_body"] = extra_body completion = self.sync_client.chat.completions.create(**api_kwargs) - + if api_kwargs.get("stream", False): return handle_streaming_response(completion) else: @@ -407,15 +411,17 @@ def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINE elif model_type == ModelType.EMBEDDER: # Extract input texts from api_kwargs texts = api_kwargs.get("input", []) - + if not texts: log.warning("😭 No input texts provided") - return EmbedderOutput(data=[], error="No input texts provided", raw_response=None) - + return EmbedderOutput( + data=[], error="No input texts provided", raw_response=None + ) + # Ensure texts is a list if isinstance(texts, str): texts = [texts] - + # Filter out empty or None texts - following HuggingFace client pattern valid_texts = [] valid_indices = [] @@ -424,37 +430,57 @@ def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINE valid_texts.append(text) valid_indices.append(i) else: - log.warning(f"🔍 Skipping empty or invalid text at index {i}: type={type(text)}, length={len(text) if hasattr(text, '__len__') else 'N/A'}, repr={repr(text)[:100]}") - + log.warning( + f"🔍 Skipping empty or invalid text at index {i}: type={type(text)}, length={len(text) if hasattr(text, '__len__') else 'N/A'}, repr={repr(text)[:100]}" + ) + if not valid_texts: log.error("😭 No valid texts found after filtering") - return EmbedderOutput(data=[], error="No valid texts found after filtering", raw_response=None) - + return EmbedderOutput( + data=[], + error="No valid texts found after filtering", + raw_response=None, + ) + if len(valid_texts) != len(texts): filtered_count = len(texts) - len(valid_texts) - log.warning(f"🔍 Filtered out {filtered_count} empty/invalid texts out of {len(texts)} total texts") - + log.warning( + f"🔍 Filtered out {filtered_count} empty/invalid texts out of {len(texts)} total texts" + ) + # Create modified api_kwargs with only valid texts filtered_api_kwargs = api_kwargs.copy() filtered_api_kwargs["input"] = valid_texts - - log.info(f"🔍 DashScope embedding API call with {len(valid_texts)} valid texts out of {len(texts)} total") - + + log.info( + f"🔍 DashScope embedding API call with {len(valid_texts)} valid texts out of {len(texts)} total" + ) + try: response = self.sync_client.embeddings.create(**filtered_api_kwargs) - log.info(f"🔍 DashScope API call successful, response type: {type(response)}") + log.info( + f"🔍 DashScope API call successful, response type: {type(response)}" + ) result = self.parse_embedding_response(response) - + # If we filtered texts, we need to create embeddings for the original indices if len(valid_texts) != len(texts): - log.info(f"🔍 Creating embeddings for {len(texts)} original positions") - + log.info( + f"🔍 Creating embeddings for {len(texts)} original positions" + ) + # Get the correct embedding dimension from the first valid embedding - embedding_dim = None # Must be determined from a successful response - if result.data and len(result.data) > 0 and hasattr(result.data[0], 'embedding'): + embedding_dim = ( + None # Must be determined from a successful response + ) + if ( + result.data + and len(result.data) > 0 + and hasattr(result.data[0], "embedding") + ): embedding_dim = len(result.data[0].embedding) log.info(f"🔍 Using embedding dimension: {embedding_dim}") - + final_data = [] valid_idx = 0 for i in range(len(texts)): @@ -464,20 +490,23 @@ def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINE valid_idx += 1 else: # Create zero embedding for filtered texts with correct dimension - log.warning(f"🔍 Creating zero embedding for filtered text at index {i}") - final_data.append(Embedding( - embedding=[0.0] * embedding_dim, # Use correct embedding dimension - index=i - )) - + log.warning( + f"🔍 Creating zero embedding for filtered text at index {i}" + ) + final_data.append( + Embedding( + embedding=[0.0] + * embedding_dim, # Use correct embedding dimension + index=i, + ) + ) + result = EmbedderOutput( - data=final_data, - error=None, - raw_response=result.raw_response + data=final_data, error=None, raw_response=result.raw_response ) - + return result - + except Exception as e: log.error(f"🔍 DashScope API call failed: {e}") return EmbedderOutput(data=[], error=str(e), raw_response=None) @@ -533,15 +562,17 @@ async def async_stream_generator(): elif model_type == ModelType.EMBEDDER: # Extract input texts from api_kwargs texts = api_kwargs.get("input", []) - + if not texts: log.warning("😭 No input texts provided") - return EmbedderOutput(data=[], error="No input texts provided", raw_response=None) - + return EmbedderOutput( + data=[], error="No input texts provided", raw_response=None + ) + # Ensure texts is a list if isinstance(texts, str): texts = [texts] - + # Filter out empty or None texts - following HuggingFace client pattern valid_texts = [] valid_indices = [] @@ -550,37 +581,57 @@ async def async_stream_generator(): valid_texts.append(text) valid_indices.append(i) else: - log.warning(f"🔍 Skipping empty or invalid text at index {i}: type={type(text)}, length={len(text) if hasattr(text, '__len__') else 'N/A'}, repr={repr(text)[:100]}") - + log.warning( + f"🔍 Skipping empty or invalid text at index {i}: type={type(text)}, length={len(text) if hasattr(text, '__len__') else 'N/A'}, repr={repr(text)[:100]}" + ) + if not valid_texts: log.error("😭 No valid texts found after filtering") - return EmbedderOutput(data=[], error="No valid texts found after filtering", raw_response=None) - + return EmbedderOutput( + data=[], + error="No valid texts found after filtering", + raw_response=None, + ) + if len(valid_texts) != len(texts): filtered_count = len(texts) - len(valid_texts) - log.warning(f"🔍 Filtered out {filtered_count} empty/invalid texts out of {len(texts)} total texts") - + log.warning( + f"🔍 Filtered out {filtered_count} empty/invalid texts out of {len(texts)} total texts" + ) + # Create modified api_kwargs with only valid texts filtered_api_kwargs = api_kwargs.copy() filtered_api_kwargs["input"] = valid_texts - - log.info(f"🔍 DashScope async embedding API call with {len(valid_texts)} valid texts out of {len(texts)} total") - + + log.info( + f"🔍 DashScope async embedding API call with {len(valid_texts)} valid texts out of {len(texts)} total" + ) + try: - response = await self.async_client.embeddings.create(**filtered_api_kwargs) - log.info(f"🔍 DashScope async API call successful, response type: {type(response)}") + response = await self.async_client.embeddings.create( + **filtered_api_kwargs + ) + log.info( + f"🔍 DashScope async API call successful, response type: {type(response)}" + ) result = self.parse_embedding_response(response) - + # If we filtered texts, we need to create embeddings for the original indices if len(valid_texts) != len(texts): - log.info(f"🔍 Creating embeddings for {len(texts)} original positions") - + log.info( + f"🔍 Creating embeddings for {len(texts)} original positions" + ) + # Get the correct embedding dimension from the first valid embedding embedding_dim = 256 # Default fallback based on config - if result.data and len(result.data) > 0 and hasattr(result.data[0], 'embedding'): + if ( + result.data + and len(result.data) > 0 + and hasattr(result.data[0], "embedding") + ): embedding_dim = len(result.data[0].embedding) log.info(f"🔍 Using embedding dimension: {embedding_dim}") - + final_data = [] valid_idx = 0 for i in range(len(texts)): @@ -590,20 +641,23 @@ async def async_stream_generator(): valid_idx += 1 else: # Create zero embedding for filtered texts with correct dimension - log.warning(f"🔍 Creating zero embedding for filtered text at index {i}") - final_data.append(Embedding( - embedding=[0.0] * embedding_dim, # Use correct embedding dimension - index=i - )) - + log.warning( + f"🔍 Creating zero embedding for filtered text at index {i}" + ) + final_data.append( + Embedding( + embedding=[0.0] + * embedding_dim, # Use correct embedding dimension + index=i, + ) + ) + result = EmbedderOutput( - data=final_data, - error=None, - raw_response=result.raw_response + data=final_data, error=None, raw_response=result.raw_response ) - + return result - + except Exception as e: log.error(f"🔍 DashScope async API call failed: {e}") return EmbedderOutput(data=[], error=str(e), raw_response=None) @@ -631,10 +685,10 @@ def __getstate__(self): """ state = self.__dict__.copy() # Remove the unpicklable client instances - if 'sync_client' in state: - del state['sync_client'] - if 'async_client' in state: - del state['async_client'] + if "sync_client" in state: + del state["sync_client"] + if "async_client" in state: + del state["async_client"] return state def __setstate__(self, state): @@ -669,7 +723,6 @@ def __init__( model_kwargs: Dict[str, Any] = {}, output_processors: Optional[DataComponent] = None, ) -> None: - super().__init__(model_kwargs=model_kwargs) if not isinstance(model_kwargs, Dict): raise TypeError( @@ -732,163 +785,204 @@ async def acall( def _compose_model_kwargs(self, **model_kwargs) -> Dict[str, object]: return F.compose_model_kwargs(self.model_kwargs, model_kwargs) + # Batch Embedding Components for DashScope class DashScopeBatchEmbedder(DataComponent): """Batch embedder specifically designed for DashScope API""" - def __init__(self, embedder, batch_size: int = 100, embedding_cache_file_name: str = "default") -> None: + def __init__( + self, + embedder, + batch_size: int = 100, + embedding_cache_file_name: str = "default", + ) -> None: super().__init__(batch_size=batch_size) self.embedder = embedder self.batch_size = batch_size if self.batch_size > 25: - log.warning(f"DashScope batch embedder initialization, batch size: {self.batch_size}, note that DashScope batch embedding size cannot exceed 25, automatically set to 25") + log.warning( + f"DashScope batch embedder initialization, batch size: {self.batch_size}, note that DashScope batch embedding size cannot exceed 25, automatically set to 25" + ) self.batch_size = 25 - self.cache_path = f'./embedding_cache/{embedding_cache_file_name}_{self.embedder.__class__.__name__}_dashscope_embeddings.pkl' + self.cache_path = f"./embedding_cache/{embedding_cache_file_name}_{self.embedder.__class__.__name__}_dashscope_embeddings.pkl" def call( - self, input: BatchEmbedderInputType, model_kwargs: Optional[Dict] = {}, force_recreate: bool = False + self, + input: BatchEmbedderInputType, + model_kwargs: Optional[Dict] = {}, + force_recreate: bool = False, ) -> BatchEmbedderOutputType: """ Batch call to DashScope embedder - + Args: input: List of input texts model_kwargs: Model parameters force_recreate: Whether to force recreation - + Returns: Batch embedding output """ # Check cache first - + if not force_recreate and os.path.exists(self.cache_path): try: - with open(self.cache_path, 'rb') as f: + with open(self.cache_path, "rb") as f: embeddings = pickle.load(f) - log.info(f"Loaded cached DashScope embeddings from: {self.cache_path}") + log.info( + f"Loaded cached DashScope embeddings from: {self.cache_path}" + ) return embeddings except Exception as e: - log.warning(f"Failed to load cache file {self.cache_path}: {e}, proceeding with fresh embedding") - + log.warning( + f"Failed to load cache file {self.cache_path}: {e}, proceeding with fresh embedding" + ) + if isinstance(input, str): input = [input] - + n = len(input) embeddings: List[EmbedderOutput] = [] - - log.info(f"Starting DashScope batch embedding processing, total {n} texts, batch size: {self.batch_size}") - + + log.info( + f"Starting DashScope batch embedding processing, total {n} texts, batch size: {self.batch_size}" + ) + for i in tqdm( range(0, n, self.batch_size), desc="DashScope batch embedding", disable=False, ): batch_input = input[i : min(i + self.batch_size, n)] - + try: # Use correct calling method: directly call embedder instance batch_output = self.embedder( input=batch_input, model_kwargs=model_kwargs ) embeddings.append(batch_output) - + # Validate batch output if batch_output.error: - log.error(f"Batch {i//self.batch_size + 1} embedding failed: {batch_output.error}") + log.error( + f"Batch {i//self.batch_size + 1} embedding failed: {batch_output.error}" + ) elif batch_output.data: - log.debug(f"Batch {i//self.batch_size + 1} successfully generated {len(batch_output.data)} embedding vectors") + log.debug( + f"Batch {i//self.batch_size + 1} successfully generated {len(batch_output.data)} embedding vectors" + ) else: - log.warning(f"Batch {i//self.batch_size + 1} returned no embedding data") - + log.warning( + f"Batch {i//self.batch_size + 1} returned no embedding data" + ) + except Exception as e: log.error(f"Batch {i//self.batch_size + 1} processing exception: {e}") # Create error embedding output - error_output = EmbedderOutput( - data=[], - error=str(e), - raw_response=None - ) + error_output = EmbedderOutput(data=[], error=str(e), raw_response=None) embeddings.append(error_output) - - log.info(f"DashScope batch embedding completed, processed {len(embeddings)} batches") - + + log.info( + f"DashScope batch embedding completed, processed {len(embeddings)} batches" + ) + # Save to cache try: - if not os.path.exists('./embedding_cache'): - os.makedirs('./embedding_cache') - with open(self.cache_path, 'wb') as f: + if not os.path.exists("./embedding_cache"): + os.makedirs("./embedding_cache") + with open(self.cache_path, "wb") as f: pickle.dump(embeddings, f) log.info(f"Saved DashScope embeddings cache to: {self.cache_path}") except Exception as e: log.warning(f"Failed to save cache to {self.cache_path}: {e}") - + return embeddings - - def __call__(self, input: BatchEmbedderInputType, model_kwargs: Optional[Dict] = {}, force_recreate: bool = False) -> BatchEmbedderOutputType: + + def __call__( + self, + input: BatchEmbedderInputType, + model_kwargs: Optional[Dict] = {}, + force_recreate: bool = False, + ) -> BatchEmbedderOutputType: """ Call operator interface, delegates to call method """ - return self.call(input=input, model_kwargs=model_kwargs, force_recreate=force_recreate) + return self.call( + input=input, model_kwargs=model_kwargs, force_recreate=force_recreate + ) class DashScopeToEmbeddings(DataComponent): """Component that converts document sequences to embedding vector sequences, specifically optimized for DashScope API""" - def __init__(self, embedder, batch_size: int = 100, force_recreate_db: bool = False, embedding_cache_file_name: str = "default") -> None: + def __init__( + self, + embedder, + batch_size: int = 100, + force_recreate_db: bool = False, + embedding_cache_file_name: str = "default", + ) -> None: super().__init__(batch_size=batch_size) self.embedder = embedder self.batch_size = batch_size - self.batch_embedder = DashScopeBatchEmbedder(embedder=embedder, batch_size=batch_size, embedding_cache_file_name=embedding_cache_file_name) + self.batch_embedder = DashScopeBatchEmbedder( + embedder=embedder, + batch_size=batch_size, + embedding_cache_file_name=embedding_cache_file_name, + ) self.force_recreate_db = force_recreate_db def __call__(self, input: List[Document]) -> List[Document]: """ Process list of documents, generating embedding vectors for each document - + Args: input: List of input documents - + Returns: List of documents containing embedding vectors """ output = deepcopy(input) - + # Convert to text list embedder_input: List[str] = [chunk.text for chunk in output] - + log.info(f"Starting to process embeddings for {len(embedder_input)} documents") - + # Batch process embeddings outputs: List[EmbedderOutput] = self.batch_embedder( - input=embedder_input, - force_recreate=self.force_recreate_db + input=embedder_input, force_recreate=self.force_recreate_db ) - + # Validate output total_embeddings = 0 error_batches = 0 - + for batch_output in outputs: if batch_output.error: error_batches += 1 log.error(f"Found error batch: {batch_output.error}") elif batch_output.data: total_embeddings += len(batch_output.data) - - log.info(f"Embedding statistics: total {total_embeddings} valid embeddings, {error_batches} error batches") - + + log.info( + f"Embedding statistics: total {total_embeddings} valid embeddings, {error_batches} error batches" + ) + # Assign embedding vectors back to documents doc_idx = 0 for batch_idx, batch_output in tqdm( - enumerate(outputs), + enumerate(outputs), desc="Assigning embedding vectors to documents", - disable=False + disable=False, ): if batch_output.error: # Create empty vectors for documents in error batches batch_size_actual = min(self.batch_size, len(output) - doc_idx) - log.warning(f"Creating empty vectors for {batch_size_actual} documents in batch {batch_idx}") - + log.warning( + f"Creating empty vectors for {batch_size_actual} documents in batch {batch_idx}" + ) + for i in range(batch_size_actual): if doc_idx < len(output): output[doc_idx].vector = [] @@ -897,33 +991,37 @@ def __call__(self, input: List[Document]) -> List[Document]: # Assign normal embedding vectors for embedding in batch_output.data: if doc_idx < len(output): - if hasattr(embedding, 'embedding'): + if hasattr(embedding, "embedding"): output[doc_idx].vector = embedding.embedding else: - log.warning(f"Invalid embedding format for document {doc_idx}") + log.warning( + f"Invalid embedding format for document {doc_idx}" + ) output[doc_idx].vector = [] doc_idx += 1 - + # Validate results valid_count = 0 empty_count = 0 - + for doc in output: - if hasattr(doc, 'vector') and doc.vector and len(doc.vector) > 0: + if hasattr(doc, "vector") and doc.vector and len(doc.vector) > 0: valid_count += 1 else: empty_count += 1 - - log.info(f"Embedding results: {valid_count} valid vectors, {empty_count} empty vectors") - + + log.info( + f"Embedding results: {valid_count} valid vectors, {empty_count} empty vectors" + ) + if valid_count == 0: log.error("❌ All documents have empty embedding vectors!") elif empty_count > 0: log.warning(f"⚠️ Found {empty_count} empty embedding vectors") else: log.info("✅ All documents successfully generated embedding vectors") - + return output def _extra_repr(self) -> str: - return f"batch_size={self.batch_size}" \ No newline at end of file + return f"batch_size={self.batch_size}" diff --git a/api/clients/google_embedder.py b/api/clients/google_embedder.py index af1b08a92..f729b356a 100644 --- a/api/clients/google_embedder.py +++ b/api/clients/google_embedder.py @@ -1,20 +1,25 @@ """Google AI Embeddings ModelClient integration.""" import os -import logging -import backoff -from typing import Dict, Any, Optional, List, Sequence +from typing import Any, Dict, Optional, Sequence +import backoff +from adalflow.utils.lazy_import import safe_import, OptionalPackages from adalflow.core.model_client import ModelClient -from adalflow.core.types import ModelType, EmbedderOutput +from adalflow.core.types import EmbedderOutput, ModelType + +# ruff: noqa: E402 +google = safe_import( + OptionalPackages.GOOGLE_GENERATIVEAI.value[0], + OptionalPackages.GOOGLE_GENERATIVEAI.value[1], +) +# ruff: noqa: E402 + +from google import generativeai as genai -try: - import google.generativeai as genai - from google.generativeai.types.text_types import EmbeddingDict, BatchEmbeddingDict -except ImportError: - raise ImportError("google-generativeai is required. Install it with 'pip install google-generativeai'") +from api.logger import get_logger -log = logging.getLogger(__name__) +log = get_logger(__name__) class GoogleEmbedderClient(ModelClient): @@ -56,7 +61,7 @@ def __init__( env_api_key_name: str = "GOOGLE_API_KEY", ): """Initialize Google AI Embeddings client. - + Args: api_key: Google AI API key. If not provided, uses environment variable. env_api_key_name: Name of environment variable containing API key. @@ -77,16 +82,16 @@ def _initialize_client(self): def parse_embedding_response(self, response) -> EmbedderOutput: """Parse Google AI embedding response to EmbedderOutput format. - + Args: response: Google AI embedding response (EmbeddingDict or BatchEmbeddingDict) - + Returns: EmbedderOutput with parsed embeddings """ try: from adalflow.core.types import Embedding - + embedding_data = [] def _extract_embedding_value(obj): @@ -113,10 +118,12 @@ def _extract_embedding_value(obj): except Exception: pass return None - + embedding_value = _extract_embedding_value(response) if embedding_value is None: - log.warning("Unexpected embedding response type/structure: %s", type(response)) + log.warning( + "Unexpected embedding response type/structure: %s", type(response) + ) embedding_data = [] elif isinstance(embedding_value, list) and len(embedding_value) > 0: if isinstance(embedding_value[0], (int, float)): @@ -142,21 +149,21 @@ def _extract_embedding_value(obj): embedding_data = [] if embedding_data: - first_dim = len(embedding_data[0].embedding) if embedding_data[0].embedding is not None else 0 - log.info("Parsed %s embedding(s) (dim=%s)", len(embedding_data), first_dim) - + first_dim = ( + len(embedding_data[0].embedding) + if embedding_data[0].embedding is not None + else 0 + ) + log.info( + "Parsed %s embedding(s) (dim=%s)", len(embedding_data), first_dim + ) + return EmbedderOutput( - data=embedding_data, - error=None, - raw_response=response + data=embedding_data, error=None, raw_response=response ) except Exception as e: log.error(f"Error parsing Google AI embedding response: {e}") - return EmbedderOutput( - data=[], - error=str(e), - raw_response=response - ) + return EmbedderOutput(data=[], error=str(e), raw_response=response) def convert_inputs_to_api_kwargs( self, @@ -165,18 +172,20 @@ def convert_inputs_to_api_kwargs( model_type: ModelType = ModelType.UNDEFINED, ) -> Dict: """Convert inputs to Google AI API format. - + Args: input: Text input(s) to embed model_kwargs: Model parameters including model name and task_type model_type: Should be ModelType.EMBEDDER for this client - + Returns: Dict: API kwargs for Google AI embedding call """ if model_type != ModelType.EMBEDDER: - raise ValueError(f"GoogleEmbedderClient only supports EMBEDDER model type, got {model_type}") - + raise ValueError( + f"GoogleEmbedderClient only supports EMBEDDER model type, got {model_type}" + ) + # Ensure input is a list if isinstance(input, str): content = [input] @@ -184,23 +193,23 @@ def convert_inputs_to_api_kwargs( content = list(input) else: raise TypeError("input must be a string or sequence of strings") - + final_model_kwargs = model_kwargs.copy() - + # Handle single vs batch embedding if len(content) == 1: final_model_kwargs["content"] = content[0] else: final_model_kwargs["contents"] = content - + # Set default task type if not provided if "task_type" not in final_model_kwargs: final_model_kwargs["task_type"] = "SEMANTIC_SIMILARITY" - + # Set default model if not provided if "model" not in final_model_kwargs: final_model_kwargs["model"] = "gemini-embedding-001" - + return final_model_kwargs @backoff.on_exception( @@ -210,28 +219,32 @@ def convert_inputs_to_api_kwargs( ) def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED): """Call Google AI embedding API. - + Args: api_kwargs: API parameters model_type: Should be ModelType.EMBEDDER - + Returns: Google AI embedding response """ if model_type != ModelType.EMBEDDER: - raise ValueError(f"GoogleEmbedderClient only supports EMBEDDER model type") - - safe_log_kwargs = {k: v for k, v in api_kwargs.items() if k not in {"content", "contents"}} + raise ValueError("GoogleEmbedderClient only supports EMBEDDER model type") + + safe_log_kwargs = { + k: v for k, v in api_kwargs.items() if k not in {"content", "contents"} + } if "content" in api_kwargs: safe_log_kwargs["content_chars"] = len(str(api_kwargs.get("content", ""))) if "contents" in api_kwargs: try: contents = api_kwargs.get("contents") - safe_log_kwargs["contents_count"] = len(contents) if hasattr(contents, "__len__") else None + safe_log_kwargs["contents_count"] = ( + len(contents) if hasattr(contents, "__len__") else None + ) except Exception: safe_log_kwargs["contents_count"] = None log.info("Google AI Embeddings call kwargs (sanitized): %s", safe_log_kwargs) - + try: # Use embed_content for single text or batch embedding if "content" in api_kwargs: @@ -245,18 +258,20 @@ def call(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINE response = genai.embed_content(content=contents, **kwargs) else: raise ValueError("Either 'content' or 'contents' must be provided") - + return response - + except Exception as e: log.error(f"Error calling Google AI Embeddings API: {e}") raise - async def acall(self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED): + async def acall( + self, api_kwargs: Dict = {}, model_type: ModelType = ModelType.UNDEFINED + ): """Async call to Google AI embedding API. - + Note: Google AI Python client doesn't have async support yet, so this falls back to synchronous call. """ # Google AI client doesn't have async support yet - return self.call(api_kwargs, model_type) \ No newline at end of file + return self.call(api_kwargs, model_type) diff --git a/api/clients/litellm.py b/api/clients/litellm.py index 1c814d11f..21d533853 100644 --- a/api/clients/litellm.py +++ b/api/clients/litellm.py @@ -1,8 +1,8 @@ import os -from typing import Optional, Callable -from openai import AsyncOpenAI, OpenAI +from typing import Callable, Optional from adalflow.components.model_client.openai_client import OpenAIClient +from openai import AsyncOpenAI, OpenAI class LiteLLMClient(OpenAIClient): @@ -34,7 +34,9 @@ def __init__( env_base_url_name: str = "LITELLM_BASE_URL", env_api_key_name: str = "LITELLM_API_KEY", ): - resolved_base_url = base_url or os.getenv(env_base_url_name, "http://localhost:4000") + resolved_base_url = base_url or os.getenv( + env_base_url_name, "http://localhost:4000" + ) if not resolved_base_url.endswith("/v1"): resolved_base_url = f"{resolved_base_url.rstrip('/')}/v1" super().__init__( diff --git a/api/clients/ollama.py b/api/clients/ollama.py index 2418e313d..b1833191c 100644 --- a/api/clients/ollama.py +++ b/api/clients/ollama.py @@ -1,25 +1,23 @@ # This file exists to patch the adalflow OllamaClient to support ollama batch embedding api `embed` -from typing import Dict, Optional, Any +from typing import Any, Dict, Optional +import backoff from adalflow.components.model_client.ollama_client import ( OllamaClient, RequestError, ResponseError, log, ) -from adalflow.core.types import EmbedderOutput, Embedding - from adalflow.core import ModelType - -import backoff +from adalflow.core.types import EmbedderOutput, Embedding def convert_inputs_to_api_kwargs( - self, - input: Optional[Any] = None, - model_kwargs: Dict = {}, - model_type: ModelType = ModelType.UNDEFINED, + self, + input: Optional[Any] = None, + model_kwargs: Dict = {}, + model_type: ModelType = ModelType.UNDEFINED, ) -> Dict: self.generate = False final_model_kwargs = model_kwargs.copy() @@ -49,21 +47,19 @@ def convert_inputs_to_api_kwargs( @backoff.on_exception( - backoff.expo, - (RequestError, ResponseError), - max_tries=5, + backoff.expo, + (RequestError, ResponseError), + max_tries=5, ) async def acall( - self, - api_kwargs: dict = None, - model_type: ModelType = ModelType.UNDEFINED, + self, + api_kwargs: dict = None, + model_type: ModelType = ModelType.UNDEFINED, ): if self.async_client is None: self.init_async_client() if self.async_client is None: - raise RuntimeError( - "Async client is not initialized" - ) + raise RuntimeError("Async client is not initialized") api_kwargs = api_kwargs or {} if model_type == ModelType.EMBEDDER: return await self.async_client.embed(**api_kwargs) @@ -79,16 +75,15 @@ async def acall( raise ValueError(f"model_type {model_type} is not supported") - @backoff.on_exception( backoff.expo, (RequestError, ResponseError), max_tries=5, ) def call( - self, - api_kwargs: dict = None, - model_type: ModelType = ModelType.UNDEFINED, + self, + api_kwargs: dict = None, + model_type: ModelType = ModelType.UNDEFINED, ): api_kwargs = api_kwargs or {} if not self.sync_client: @@ -109,23 +104,23 @@ def call( raise ValueError(f"model_type {model_type} is not supported") -def parse_embedding_response( - self, response: Dict[str, list[float]] -) -> EmbedderOutput: +def parse_embedding_response(self, response: Dict[str, list[float]]) -> EmbedderOutput: r"""Parse the embedding response to a structure AdalFlow components can understand. Pull the embedding from response['embedding'] and store it Embedding dataclass """ try: - return EmbedderOutput(data=[ - Embedding(embedding=emb, index=i) - for i, emb in enumerate(response["embeddings"]) - ]) + return EmbedderOutput( + data=[ + Embedding(embedding=emb, index=i) + for i, emb in enumerate(response["embeddings"]) + ] + ) except Exception as e: log.error(f"Error parsing the embedding response: {e}") return EmbedderOutput(data=[], error=str(e), raw_response=response) + OllamaClient.convert_inputs_to_api_kwargs = convert_inputs_to_api_kwargs OllamaClient.parse_embedding_response = parse_embedding_response OllamaClient.call = call OllamaClient.acall = acall - diff --git a/api/clients/openrouter.py b/api/clients/openrouter.py index b71c47744..56fbde86a 100644 --- a/api/clients/openrouter.py +++ b/api/clients/openrouter.py @@ -1,20 +1,21 @@ """OpenRouter ModelClient integration.""" -from typing import Dict, Sequence, Optional, Any, List -import logging import json -import aiohttp -import requests -from requests.exceptions import RequestException, Timeout +from typing import Any, Dict +import aiohttp from adalflow.core.model_client import ModelClient from adalflow.core.types import ( CompletionUsage, - ModelType, GeneratorOutput, + ModelType, ) +from requests.exceptions import RequestException + +from api.logger import get_logger + +log = get_logger(__name__) -log = logging.getLogger(__name__) class OpenRouterClient(ModelClient): __doc__ = r"""A component wrapper for the OpenRouter API client. @@ -45,28 +46,24 @@ def __init__(self, *args, **kwargs) -> None: def init_sync_client(self): """Initialize the synchronous OpenRouter client.""" from api.config import OPENROUTER_API_KEY + api_key = OPENROUTER_API_KEY if not api_key: log.warning("OPENROUTER_API_KEY not configured") # OpenRouter doesn't have a dedicated client library, so we'll use requests directly - return { - "api_key": api_key, - "base_url": "https://openrouter.ai/api/v1" - } + return {"api_key": api_key, "base_url": "https://openrouter.ai/api/v1"} def init_async_client(self): """Initialize the asynchronous OpenRouter client.""" from api.config import OPENROUTER_API_KEY + api_key = OPENROUTER_API_KEY if not api_key: log.warning("OPENROUTER_API_KEY not configured") # For async, we'll use aiohttp - return { - "api_key": api_key, - "base_url": "https://openrouter.ai/api/v1" - } + return {"api_key": api_key, "base_url": "https://openrouter.ai/api/v1"} def convert_inputs_to_api_kwargs( self, input: Any, model_kwargs: Dict = None, model_type: ModelType = None @@ -81,18 +78,19 @@ def convert_inputs_to_api_kwargs( # Convert input to messages format if it's a string if isinstance(input, str): messages = [{"role": "user", "content": input}] - elif isinstance(input, list) and all(isinstance(msg, dict) for msg in input): + elif isinstance(input, list) and all( + isinstance(msg, dict) for msg in input + ): messages = input else: - raise ValueError(f"Unsupported input format for OpenRouter: {type(input)}") + raise ValueError( + f"Unsupported input format for OpenRouter: {type(input)}" + ) # For debugging log.info(f"Messages for OpenRouter: {messages}") - api_kwargs = { - "messages": messages, - **model_kwargs - } + api_kwargs = {"messages": messages, **model_kwargs} # Ensure model is specified if "model" not in api_kwargs: @@ -104,7 +102,9 @@ def convert_inputs_to_api_kwargs( # OpenRouter doesn't support embeddings directly # We could potentially use a specific model through OpenRouter for embeddings # but for now, we'll raise an error - raise NotImplementedError("OpenRouter client does not support embeddings yet") + raise NotImplementedError( + "OpenRouter client does not support embeddings yet" + ) else: raise ValueError(f"Unsupported model type: {model_type}") @@ -118,10 +118,12 @@ async def acall(self, api_kwargs: Dict = None, model_type: ModelType = None) -> if not self.async_client.get("api_key"): error_msg = "OPENROUTER_API_KEY not configured. Please set this environment variable to use OpenRouter." log.error(error_msg) + # Instead of raising an exception, return a generator that yields the error message # This allows the error to be displayed to the user in the streaming response async def error_generator(): yield error_msg + return error_generator() api_kwargs = api_kwargs or {} @@ -132,7 +134,7 @@ async def error_generator(): "Authorization": f"Bearer {self.async_client['api_key']}", "Content-Type": "application/json", "HTTP-Referer": "https://github.com/AsyncFuncAI/deepwiki-open", # Optional - "X-Title": "DeepWiki" # Optional + "X-Title": "DeepWiki", # Optional } # Always use non-streaming mode for OpenRouter @@ -140,8 +142,9 @@ async def error_generator(): # Make the API call try: - log.info(f"Making async OpenRouter API call to {self.async_client['base_url']}/chat/completions") - log.info(f"Request headers: {headers}") + log.info( + f"Making async OpenRouter API call to {self.async_client['base_url']}/chat/completions" + ) log.info(f"Request body: {api_kwargs}") async with aiohttp.ClientSession() as session: @@ -150,15 +153,18 @@ async def error_generator(): f"{self.async_client['base_url']}/chat/completions", headers=headers, json=api_kwargs, - timeout=60 + timeout=60, ) as response: if response.status != 200: error_text = await response.text() - log.error(f"OpenRouter API error ({response.status}): {error_text}") + log.error( + f"OpenRouter API error ({response.status}): {error_text}" + ) # Return a generator that yields the error message async def error_response_generator(): yield f"OpenRouter API error ({response.status}): {error_text}" + return error_response_generator() # Get the full response @@ -169,12 +175,18 @@ async def error_response_generator(): async def content_generator(): if "choices" in data and len(data["choices"]) > 0: choice = data["choices"][0] - if "message" in choice and "content" in choice["message"]: + if ( + "message" in choice + and "content" in choice["message"] + ): content = choice["message"]["content"] log.info("Successfully retrieved response") # Check if the content is XML and ensure it's properly formatted - if content.strip().startswith("<") and ">" in content: + if ( + content.strip().startswith("<") + and ">" in content + ): # It's likely XML, let's make sure it's properly formatted try: # Extract the XML content @@ -182,11 +194,17 @@ async def content_generator(): # Check if it's a wiki_structure XML if "" in xml_content: - log.info("Found wiki_structure XML, ensuring proper format") + log.info( + "Found wiki_structure XML, ensuring proper format" + ) # Extract just the wiki_structure XML import re - wiki_match = re.search(r'[\s\S]*?<\/wiki_structure>', xml_content) + + wiki_match = re.search( + r"[\s\S]*?<\/wiki_structure>", + xml_content, + ) if wiki_match: # Get the raw XML raw_xml = wiki_match.group(0) @@ -201,26 +219,52 @@ async def content_generator(): fixed_xml = clean_xml # Replace & with & if not already part of an entity - fixed_xml = re.sub(r'&(?!amp;|lt;|gt;|apos;|quot;)', '&', fixed_xml) + fixed_xml = re.sub( + r"&(?!amp;|lt;|gt;|apos;|quot;)", + "&", + fixed_xml, + ) # Fix other common XML issues - fixed_xml = fixed_xml.replace('', '>') + fixed_xml = ( + fixed_xml.replace( + "", ">") + ) # Try to parse the fixed XML - from xml.dom.minidom import parseString + from xml.dom.minidom import ( + parseString, + ) + dom = parseString(fixed_xml) # Get the pretty-printed XML with proper indentation - pretty_xml = dom.toprettyxml() + pretty_xml = ( + dom.toprettyxml() + ) # Remove XML declaration - if pretty_xml.startswith('')+2:].strip() - - log.info(f"Extracted and validated XML: {pretty_xml[:100]}...") + if pretty_xml.startswith( + "" + ) + + 2 : + ].strip() + + log.info( + f"Extracted and validated XML: {pretty_xml[:100]}..." + ) yield pretty_xml - except Exception as xml_parse_error: - log.warning(f"XML validation failed: {str(xml_parse_error)}, using raw XML") + except ( + Exception + ) as xml_parse_error: + log.warning( + f"XML validation failed: {str(xml_parse_error)}, using raw XML" + ) # If XML validation fails, try a more aggressive approach try: @@ -228,61 +272,116 @@ async def content_generator(): import re # Extract the basic structure - structure_match = re.search(r'(.*?)', clean_xml, re.DOTALL) + structure_match = re.search( + r"(.*?)", + clean_xml, + re.DOTALL, + ) if structure_match: - structure = structure_match.group(1).strip() + structure = structure_match.group( + 1 + ).strip() # Rebuild a clean XML structure clean_structure = "\n" # Extract title - title_match = re.search(r'(.*?)', structure, re.DOTALL) + title_match = re.search( + r"(.*?)", + structure, + re.DOTALL, + ) if title_match: - title = title_match.group(1).strip() + title = title_match.group( + 1 + ).strip() clean_structure += f" {title}\n" # Extract description - desc_match = re.search(r'(.*?)', structure, re.DOTALL) + desc_match = re.search( + r"(.*?)", + structure, + re.DOTALL, + ) if desc_match: - desc = desc_match.group(1).strip() + desc = desc_match.group( + 1 + ).strip() clean_structure += f" {desc}\n" # Add pages section - clean_structure += " \n" + clean_structure += ( + " \n" + ) # Extract pages - pages = re.findall(r'(.*?)', structure, re.DOTALL) - for page_id, page_content in pages: + pages = re.findall( + r'(.*?)', + structure, + re.DOTALL, + ) + for ( + page_id, + page_content, + ) in pages: clean_structure += f' \n' # Extract page title - page_title_match = re.search(r'(.*?)', page_content, re.DOTALL) + page_title_match = re.search( + r"(.*?)", + page_content, + re.DOTALL, + ) if page_title_match: - page_title = page_title_match.group(1).strip() + page_title = page_title_match.group( + 1 + ).strip() clean_structure += f" {page_title}\n" # Extract page description - page_desc_match = re.search(r'(.*?)', page_content, re.DOTALL) + page_desc_match = re.search( + r"(.*?)", + page_content, + re.DOTALL, + ) if page_desc_match: - page_desc = page_desc_match.group(1).strip() + page_desc = page_desc_match.group( + 1 + ).strip() clean_structure += f" {page_desc}\n" # Extract importance - importance_match = re.search(r'(.*?)', page_content, re.DOTALL) + importance_match = re.search( + r"(.*?)", + page_content, + re.DOTALL, + ) if importance_match: - importance = importance_match.group(1).strip() + importance = importance_match.group( + 1 + ).strip() clean_structure += f" {importance}\n" # Extract relevant files clean_structure += " \n" - file_paths = re.findall(r'(.*?)', page_content, re.DOTALL) - for file_path in file_paths: + file_paths = re.findall( + r"(.*?)", + page_content, + re.DOTALL, + ) + for ( + file_path + ) in file_paths: clean_structure += f" {file_path.strip()}\n" clean_structure += " \n" # Extract related pages clean_structure += " \n" - related_pages = re.findall(r'(.*?)', page_content, re.DOTALL) + related_pages = re.findall( + r"(.*?)", + page_content, + re.DOTALL, + ) for related in related_pages: clean_structure += f" {related.strip()}\n" clean_structure += " \n" @@ -291,23 +390,35 @@ async def content_generator(): clean_structure += " \n" - log.info("Successfully rebuilt clean XML structure") + log.info( + "Successfully rebuilt clean XML structure" + ) yield clean_structure else: - log.warning("Could not extract wiki structure, using raw XML") + log.warning( + "Could not extract wiki structure, using raw XML" + ) yield clean_xml - except Exception as rebuild_error: - log.warning(f"Failed to rebuild XML: {str(rebuild_error)}, using raw XML") + except ( + Exception + ) as rebuild_error: + log.warning( + f"Failed to rebuild XML: {str(rebuild_error)}, using raw XML" + ) yield clean_xml else: # If we can't extract it, just yield the original content - log.warning("Could not extract wiki_structure XML, yielding original content") + log.warning( + "Could not extract wiki_structure XML, yielding original content" + ) yield xml_content else: # For other XML content, just yield it as is yield content except Exception as xml_error: - log.error(f"Error processing XML content: {str(xml_error)}") + log.error( + f"Error processing XML content: {str(xml_error)}" + ) yield content else: # Not XML, just yield the content @@ -322,11 +433,14 @@ async def content_generator(): return content_generator() except aiohttp.ClientError as e: e_client = e - log.error(f"Connection error with OpenRouter API: {str(e_client)}") + log.error( + f"Connection error with OpenRouter API: {str(e_client)}" + ) # Return a generator that yields the error message async def connection_error_generator(): yield f"Connection error with OpenRouter API: {str(e_client)}. Please check your internet connection and that the OpenRouter API is accessible." + return connection_error_generator() except RequestException as e: @@ -336,15 +450,19 @@ async def connection_error_generator(): # Return a generator that yields the error message async def request_error_generator(): yield f"Error calling OpenRouter API: {str(e_req)}" + return request_error_generator() except Exception as e: e_unexp = e - log.error(f"Unexpected error calling OpenRouter API asynchronously: {str(e_unexp)}") + log.error( + f"Unexpected error calling OpenRouter API asynchronously: {str(e_unexp)}" + ) # Return a generator that yields the error message async def unexpected_error_generator(): yield f"Unexpected error calling OpenRouter API: {str(e_unexp)}" + return unexpected_error_generator() else: @@ -354,6 +472,7 @@ async def unexpected_error_generator(): # Return a generator that yields the error message async def model_type_error_generator(): yield error_msg + return model_type_error_generator() def _process_completion_response(self, data: Dict) -> GeneratorOutput: @@ -370,7 +489,9 @@ def _process_completion_response(self, data: Dict) -> GeneratorOutput: elif "text" in choice: content = choice.get("text", "") else: - raise ValueError(f"Unexpected response format from OpenRouter: {choice}") + raise ValueError( + f"Unexpected response format from OpenRouter: {choice}" + ) # Extract usage information if available usage = None @@ -378,15 +499,11 @@ def _process_completion_response(self, data: Dict) -> GeneratorOutput: usage = CompletionUsage( prompt_tokens=data["usage"].get("prompt_tokens", 0), completion_tokens=data["usage"].get("completion_tokens", 0), - total_tokens=data["usage"].get("total_tokens", 0) + total_tokens=data["usage"].get("total_tokens", 0), ) # Create and return the GeneratorOutput - return GeneratorOutput( - data=content, - usage=usage, - raw_response=data - ) + return GeneratorOutput(data=content, usage=usage, raw_response=data) except Exception as e_proc: log.error(f"Error processing OpenRouter completion response: {str(e_proc)}") @@ -404,8 +521,8 @@ def _process_streaming_response(self, response): buffer += chunk # Process complete lines in the buffer - while '\n' in buffer: - line, buffer = buffer.split('\n', 1) + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) line = line.strip() if not line: @@ -414,7 +531,7 @@ def _process_streaming_response(self, response): log.debug(f"Processing line: {line}") # Skip SSE comments (lines starting with :) - if line.startswith(':'): + if line.startswith(":"): log.debug(f"Skipping SSE comment: {line}") continue @@ -431,18 +548,29 @@ def _process_streaming_response(self, response): log.debug(f"Parsed JSON data: {data_obj}") # Extract content from delta - if "choices" in data_obj and len(data_obj["choices"]) > 0: + if ( + "choices" in data_obj + and len(data_obj["choices"]) > 0 + ): choice = data_obj["choices"][0] - if "delta" in choice and "content" in choice["delta"] and choice["delta"]["content"]: + if ( + "delta" in choice + and "content" in choice["delta"] + and choice["delta"]["content"] + ): content = choice["delta"]["content"] log.debug(f"Yielding delta content: {content}") yield content elif "text" in choice: - log.debug(f"Yielding text content: {choice['text']}") + log.debug( + f"Yielding text content: {choice['text']}" + ) yield choice["text"] else: - log.debug(f"No content found in choice: {choice}") + log.debug( + f"No content found in choice: {choice}" + ) else: log.debug(f"No choices found in data: {data_obj}") @@ -465,15 +593,15 @@ async def _process_async_streaming_response(self, response): try: # Convert bytes to string and add to buffer if isinstance(chunk, bytes): - chunk_str = chunk.decode('utf-8') + chunk_str = chunk.decode("utf-8") else: chunk_str = str(chunk) buffer += chunk_str # Process complete lines in the buffer - while '\n' in buffer: - line, buffer = buffer.split('\n', 1) + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) line = line.strip() if not line: @@ -482,7 +610,7 @@ async def _process_async_streaming_response(self, response): log.debug(f"Processing line: {line}") # Skip SSE comments (lines starting with :) - if line.startswith(':'): + if line.startswith(":"): log.debug(f"Skipping SSE comment: {line}") continue @@ -499,18 +627,29 @@ async def _process_async_streaming_response(self, response): log.debug(f"Parsed JSON data: {data_obj}") # Extract content from delta - if "choices" in data_obj and len(data_obj["choices"]) > 0: + if ( + "choices" in data_obj + and len(data_obj["choices"]) > 0 + ): choice = data_obj["choices"][0] - if "delta" in choice and "content" in choice["delta"] and choice["delta"]["content"]: + if ( + "delta" in choice + and "content" in choice["delta"] + and choice["delta"]["content"] + ): content = choice["delta"]["content"] log.debug(f"Yielding delta content: {content}") yield content elif "text" in choice: - log.debug(f"Yielding text content: {choice['text']}") + log.debug( + f"Yielding text content: {choice['text']}" + ) yield choice["text"] else: - log.debug(f"No content found in choice: {choice}") + log.debug( + f"No content found in choice: {choice}" + ) else: log.debug(f"No choices found in data: {data_obj}") diff --git a/api/config.py b/api/config.py index bd55b7247..d6f9232a7 100644 --- a/api/config.py +++ b/api/config.py @@ -1,13 +1,11 @@ -import os import json -import logging +import os import re from pathlib import Path -from typing import List, Union, Dict, Any - -logger = logging.getLogger(__name__) +from typing import TYPE_CHECKING, Any, Dict, List, Union from api.clients import ( + AnthropicBedrockClient, AzureAIClient, BedrockClient, DashscopeClient, @@ -18,17 +16,25 @@ OpenAIClient, OpenRouterClient, ) +from api.logger import get_logger + +if TYPE_CHECKING: + from adalflow import Embedder + + +logger = get_logger(__name__) + # Get API keys from environment variables -OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY') -LITELLM_API_KEY = os.environ.get('LITELLM_API_KEY') -GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY') -OPENROUTER_API_KEY = os.environ.get('OPENROUTER_API_KEY') -AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID') -AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY') -AWS_SESSION_TOKEN = os.environ.get('AWS_SESSION_TOKEN') -AWS_REGION = os.environ.get('AWS_REGION') -AWS_ROLE_ARN = os.environ.get('AWS_ROLE_ARN') +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") +LITELLM_API_KEY = os.environ.get("LITELLM_API_KEY") +GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY") +OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY") +AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") +AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") +AWS_SESSION_TOKEN = os.environ.get("AWS_SESSION_TOKEN") +AWS_REGION = os.environ.get("AWS_REGION") +AWS_ROLE_ARN = os.environ.get("AWS_ROLE_ARN") # Set keys in environment (in case they're needed elsewhere in the code) if OPENAI_API_KEY: @@ -51,30 +57,47 @@ os.environ["AWS_ROLE_ARN"] = AWS_ROLE_ARN # Wiki authentication settings -raw_auth_mode = os.environ.get('DEEPWIKI_AUTH_MODE', 'False') -WIKI_AUTH_MODE = raw_auth_mode.lower() in ['true', '1', 't'] -WIKI_AUTH_CODE = os.environ.get('DEEPWIKI_AUTH_CODE', '') +raw_auth_mode = os.environ.get("DEEPWIKI_AUTH_MODE", "False") +WIKI_AUTH_MODE = raw_auth_mode.lower() in ["true", "1", "t"] +WIKI_AUTH_CODE = os.environ.get("DEEPWIKI_AUTH_CODE", "") # Embedder settings -EMBEDDER_TYPE = os.environ.get('DEEPWIKI_EMBEDDER_TYPE', 'openai').lower() +EMBEDDER_TYPE = os.environ.get("DEEPWIKI_EMBEDDER_TYPE", "openai").lower() # Get configuration directory from environment variable, or use default if not set -CONFIG_DIR = os.environ.get('DEEPWIKI_CONFIG_DIR', None) +CONFIG_DIR = os.environ.get("DEEPWIKI_CONFIG_DIR", None) # Client class mapping CLIENT_CLASSES = { - "GoogleGenAIClient": GoogleGenAIClient, - "GoogleEmbedderClient": GoogleEmbedderClient, - "OpenAIClient": OpenAIClient, - "LiteLLMClient" : LiteLLMClient, - "OpenRouterClient": OpenRouterClient, - "OllamaClient": OllamaClient, - "BedrockClient": BedrockClient, - "AzureAIClient": AzureAIClient, - "DashscopeClient": DashscopeClient + GoogleGenAIClient.__name__: GoogleGenAIClient, + GoogleEmbedderClient.__name__: GoogleEmbedderClient, + OpenAIClient.__name__: OpenAIClient, + LiteLLMClient.__name__: LiteLLMClient, + OpenRouterClient.__name__: OpenRouterClient, + OllamaClient.__name__: OllamaClient, + BedrockClient.__name__: BedrockClient, + AzureAIClient.__name__: AzureAIClient, + DashscopeClient.__name__: DashscopeClient, + AnthropicBedrockClient.__name__: AnthropicBedrockClient, +} + + +_DEFAULT_PROVIDER_MAP = { + "google": GoogleGenAIClient, + "openai": OpenAIClient, + "litellm": LiteLLMClient, + "openrouter": OpenRouterClient, + "ollama": OllamaClient, + "bedrock": BedrockClient, + "azure": AzureAIClient, + "dashscope": DashscopeClient, + "anthropic": AnthropicBedrockClient, } -def replace_env_placeholders(config: Union[Dict[str, Any], List[Any], str, Any]) -> Union[Dict[str, Any], List[Any], str, Any]: + +def replace_env_placeholders( + config: Union[Dict[str, Any], List[Any], str, Any], +) -> Union[Dict[str, Any], List[Any], str, Any]: """ Recursively replace placeholders like "${ENV_VAR}" in string values within a nested configuration structure (dicts, lists, strings) @@ -104,6 +127,7 @@ def replacer(match: re.Match[str]) -> str: # Handles numbers, booleans, None, etc. return config + # Load JSON configuration file def load_json_config(filename): try: @@ -120,7 +144,7 @@ def load_json_config(filename): logger.warning(f"Configuration file {config_path} does not exist") return {} - with open(config_path, 'r', encoding='utf-8') as f: + with open(config_path, "r", encoding="utf-8") as f: config = json.load(f) config = replace_env_placeholders(config) return config @@ -128,6 +152,7 @@ def load_json_config(filename): logger.error(f"Error loading configuration file {filename}: {str(e)}") return {} + # Load generator model configuration def load_generator_config(): generator_config = load_json_config("generator.json") @@ -137,25 +162,18 @@ def load_generator_config(): for provider_id, provider_config in generator_config["providers"].items(): # Try to set client class from client_class if provider_config.get("client_class") in CLIENT_CLASSES: - provider_config["model_client"] = CLIENT_CLASSES[provider_config["client_class"]] + provider_config["model_client"] = CLIENT_CLASSES[ + provider_config["client_class"] + ] # Fall back to default mapping based on provider_id - elif provider_id in ["google", "openai", "openrouter", "ollama", "bedrock", "azure", "dashscope", "litellm"]: - default_map = { - "google": GoogleGenAIClient, - "openai": OpenAIClient, - "litellm": LiteLLMClient, - "openrouter": OpenRouterClient, - "ollama": OllamaClient, - "bedrock": BedrockClient, - "azure": AzureAIClient, - "dashscope": DashscopeClient - } - provider_config["model_client"] = default_map[provider_id] + elif provider_id in _DEFAULT_PROVIDER_MAP: + provider_config["model_client"] = _DEFAULT_PROVIDER_MAP[provider_id] else: logger.warning(f"Unknown provider or client class: {provider_id}") return generator_config + # Load embedder configuration def load_embedder_config(): embedder_config = load_json_config("embedder.json") @@ -169,6 +187,7 @@ def load_embedder_config(): return embedder_config + def get_embedder_config(): """ Get the current embedder configuration based on DEEPWIKI_EMBEDDER_TYPE. @@ -177,15 +196,16 @@ def get_embedder_config(): dict: The embedder configuration with model_client resolved """ embedder_type = EMBEDDER_TYPE - if embedder_type == 'bedrock' and 'embedder_bedrock' in configs: + if embedder_type == "bedrock" and "embedder_bedrock" in configs: return configs.get("embedder_bedrock", {}) - elif embedder_type == 'google' and 'embedder_google' in configs: + elif embedder_type == "google" and "embedder_google" in configs: return configs.get("embedder_google", {}) - elif embedder_type == 'ollama' and 'embedder_ollama' in configs: + elif embedder_type == "ollama" and "embedder_ollama" in configs: return configs.get("embedder_ollama", {}) else: return configs.get("embedder", {}) + def is_ollama_embedder(): """ Check if the current embedder configuration uses OllamaClient. @@ -206,6 +226,7 @@ def is_ollama_embedder(): client_class = embedder_config.get("client_class", "") return client_class == "OllamaClient" + def is_google_embedder(): """ Check if the current embedder configuration uses GoogleEmbedderClient. @@ -226,6 +247,7 @@ def is_google_embedder(): client_class = embedder_config.get("client_class", "") return client_class == "GoogleEmbedderClient" + def is_bedrock_embedder(): """ Check if the current embedder configuration uses BedrockClient. @@ -244,26 +266,29 @@ def is_bedrock_embedder(): client_class = embedder_config.get("client_class", "") return client_class == "BedrockClient" + def get_embedder_type(): """ Get the current embedder type based on configuration. - + Returns: str: 'bedrock', 'ollama', 'google', or 'openai' (default) """ if is_bedrock_embedder(): - return 'bedrock' + return "bedrock" elif is_ollama_embedder(): - return 'ollama' + return "ollama" elif is_google_embedder(): - return 'google' + return "google" else: - return 'openai' + return "openai" + # Load repository and file filters configuration def load_repo_config(): return load_json_config("repo.json") + # Load language configuration def load_lang_config(): default_config = { @@ -277,62 +302,26 @@ def load_lang_config(): "vi": "Vietnamese (Tiếng Việt)", "pt-br": "Brazilian Portuguese (Português Brasileiro)", "fr": "Français (French)", - "ru": "Русский (Russian)" + "ru": "Русский (Russian)", }, - "default": "en" + "default": "en", } - loaded_config = load_json_config("lang.json") # Let load_json_config handle path and loading + loaded_config = load_json_config( + "lang.json" + ) # Let load_json_config handle path and loading if not loaded_config: return default_config if "supported_languages" not in loaded_config or "default" not in loaded_config: - logger.warning("Language configuration file 'lang.json' is malformed. Using default language configuration.") + logger.warning( + "Language configuration file 'lang.json' is malformed. Using default language configuration." + ) return default_config return loaded_config -# Default excluded directories and files -DEFAULT_EXCLUDED_DIRS: List[str] = [ - # Virtual environments and package managers - "./.venv/", "./venv/", "./env/", "./virtualenv/", - "./node_modules/", "./bower_components/", "./jspm_packages/", - # Version control - "./.git/", "./.svn/", "./.hg/", "./.bzr/", - # Cache and compiled files - "./__pycache__/", "./.pytest_cache/", "./.mypy_cache/", "./.ruff_cache/", "./.coverage/", - # Build and distribution - "./dist/", "./build/", "./out/", "./target/", "./bin/", "./obj/", - # Documentation - "./docs/", "./_docs/", "./site-docs/", "./_site/", - # IDE specific - "./.idea/", "./.vscode/", "./.vs/", "./.eclipse/", "./.settings/", - # Logs and temporary files - "./logs/", "./log/", "./tmp/", "./temp/", -] - -DEFAULT_EXCLUDED_FILES: List[str] = [ - "yarn.lock", "pnpm-lock.yaml", "npm-shrinkwrap.json", "poetry.lock", - "Pipfile.lock", "requirements.txt.lock", "Cargo.lock", "composer.lock", - ".lock", ".DS_Store", "Thumbs.db", "desktop.ini", "*.lnk", ".env", - ".env.*", "*.env", "*.cfg", "*.ini", ".flaskenv", ".gitignore", - ".gitattributes", ".gitmodules", ".github", ".gitlab-ci.yml", - ".prettierrc", ".eslintrc", ".eslintignore", ".stylelintrc", - ".editorconfig", ".jshintrc", ".pylintrc", ".flake8", "mypy.ini", - "pyproject.toml", "tsconfig.json", "webpack.config.js", "babel.config.js", - "rollup.config.js", "jest.config.js", "karma.conf.js", "vite.config.js", - "next.config.js", "*.min.js", "*.min.css", "*.bundle.js", "*.bundle.css", - "*.map", "*.gz", "*.zip", "*.tar", "*.tgz", "*.rar", "*.7z", "*.iso", - "*.dmg", "*.img", "*.msix", "*.appx", "*.appxbundle", "*.xap", "*.ipa", - "*.deb", "*.rpm", "*.msi", "*.exe", "*.dll", "*.so", "*.dylib", "*.o", - "*.obj", "*.jar", "*.war", "*.ear", "*.jsm", "*.class", "*.pyc", "*.pyd", - "*.pyo", "__pycache__", "*.a", "*.lib", "*.lo", "*.la", "*.slo", "*.dSYM", - "*.egg", "*.egg-info", "*.dist-info", "*.eggs", "node_modules", - "bower_components", "jspm_packages", "lib-cov", "coverage", "htmlcov", - ".nyc_output", ".tox", "dist", "build", "bld", "out", "bin", "target", - "packages/*/dist", "packages/*/build", ".output" -] # Initialize empty configuration configs = {} @@ -350,13 +339,20 @@ def load_lang_config(): # Update embedder configuration if embedder_config: - for key in ["embedder", "embedder_ollama", "embedder_google", "embedder_bedrock", "retriever", "text_splitter"]: + for key in [ + "embedder", + "embedder_ollama", + "embedder_google", + "embedder_bedrock", + "retriever", + "text_splitter", + ]: if key in embedder_config: configs[key] = embedder_config[key] # Update repository configuration if repo_config: - for key in ["file_filters", "repository"]: + for key in ["file_filters", "repository", "code_extensions", "doc_extensions"]: if key in repo_config: configs[key] = repo_config[key] @@ -419,3 +415,67 @@ def get_model_config(provider="google", model=None): result["model_kwargs"] = {"model": model, **model_params} return result + + +def get_embedder( + is_local_ollama: bool = False, + use_google_embedder: bool = False, + embedder_type: str = None, +) -> "Embedder": + """Get embedder based on configuration or parameters. + + Args: + is_local_ollama: Legacy parameter for Ollama embedder + use_google_embedder: Legacy parameter for Google embedder + embedder_type: Direct specification of embedder type ('ollama', 'google', 'bedrock', 'openai') + + Returns: + adal.Embedder: Configured embedder instance + """ + # Determine which embedder config to use + from adalflow import Embedder + + if embedder_type: + if embedder_type == "ollama": + embedder_config = configs["embedder_ollama"] + elif embedder_type == "google": + embedder_config = configs["embedder_google"] + elif embedder_type == "bedrock": + embedder_config = configs["embedder_bedrock"] + else: # default to openai + embedder_config = configs["embedder"] + elif is_local_ollama: + embedder_config = configs["embedder_ollama"] + elif use_google_embedder: + embedder_config = configs["embedder_google"] + else: + # Auto-detect based on current configuration + current_type = get_embedder_type() + if current_type == "bedrock": + embedder_config = configs["embedder_bedrock"] + elif current_type == "ollama": + embedder_config = configs["embedder_ollama"] + elif current_type == "google": + embedder_config = configs["embedder_google"] + else: + embedder_config = configs["embedder"] + + # --- Initialize Embedder --- + model_client_class = embedder_config["model_client"] + if "initialize_kwargs" in embedder_config: + model_client = model_client_class(**embedder_config["initialize_kwargs"]) + else: + model_client = model_client_class() + + # Create embedder with basic parameters + embedder_kwargs = { + "model_client": model_client, + "model_kwargs": embedder_config["model_kwargs"], + } + + embedder = Embedder(**embedder_kwargs) + + # Set batch_size as an attribute if available (not a constructor parameter) + if "batch_size" in embedder_config: + embedder.batch_size = embedder_config["batch_size"] + return embedder diff --git a/api/config/repo.json b/api/config/repo.json index b1afea014..362371e2d 100644 --- a/api/config/repo.json +++ b/api/config/repo.json @@ -1,128 +1,172 @@ { "file_filters": { "excluded_dirs": [ - "./.venv/", - "./venv/", - "./env/", + "./.venv/", + "./venv/", + "./env/", "./virtualenv/", - "./node_modules/", - "./bower_components/", + "./node_modules/", + "./bower_components/", "./jspm_packages/", - "./.git/", - "./.svn/", - "./.hg/", + "./.git/", + "./.svn/", + "./.hg/", "./.bzr/", - "./vendor/" + "./vendor/", + "./__pycache__/", + "./.pytest_cache/", + "./.mypy_cache/", + "./.ruff_cache/", + "./.coverage/", + "./dist/", + "./build/", + "./out/", + "./target/", + "./bin/", + "./obj/", + "./docs/", + "./_docs/", + "./site-docs/", + "./_site/", + "./.idea/", + "./.vscode/", + "./.vs/", + "./.eclipse/", + "./.settings/", + "./logs/", + "./log/", + "./tmp/", + "./temp/" ], "excluded_files": [ - "yarn.lock", - "pnpm-lock.yaml", - "npm-shrinkwrap.json", + "yarn.lock", + "pnpm-lock.yaml", + "npm-shrinkwrap.json", "poetry.lock", - "Pipfile.lock", - "requirements.txt.lock", - "Cargo.lock", + "Pipfile.lock", + "requirements.txt.lock", + "Cargo.lock", "composer.lock", - ".lock", - ".DS_Store", - "Thumbs.db", - "desktop.ini", - "*.lnk", - ".env", - ".env.*", - "*.env", - "*.cfg", - "*.ini", - ".flaskenv", - ".gitignore", - ".gitattributes", - ".gitmodules", - ".github", - ".gitlab-ci.yml", - ".prettierrc", - ".eslintrc", - ".eslintignore", - ".stylelintrc", - ".editorconfig", - ".jshintrc", - ".pylintrc", - ".flake8", - "mypy.ini", - "pyproject.toml", - "tsconfig.json", - "webpack.config.js", - "babel.config.js", - "rollup.config.js", - "jest.config.js", - "karma.conf.js", - "vite.config.js", - "next.config.js", - "*.min.js", - "*.min.css", - "*.bundle.js", - "*.bundle.css", - "*.map", - "*.gz", - "*.zip", - "*.tar", - "*.tgz", - "*.rar", - "*.7z", - "*.iso", - "*.dmg", - "*.img", - "*.msix", - "*.appx", - "*.appxbundle", - "*.xap", - "*.ipa", - "*.deb", - "*.rpm", - "*.msi", - "*.exe", - "*.dll", - "*.so", - "*.dylib", - "*.o", - "*.obj", - "*.jar", - "*.war", - "*.ear", - "*.jsm", - "*.class", - "*.pyc", - "*.pyd", - "*.pyo", - "__pycache__", - "*.a", - "*.lib", - "*.lo", - "*.la", - "*.slo", + ".lock", + ".DS_Store", + "Thumbs.db", + "desktop.ini", + "*.lnk", + ".env", + ".env.*", + "*.env", + "*.cfg", + "*.ini", + ".flaskenv", + ".gitignore", + ".gitattributes", + ".gitmodules", + ".github", + ".gitlab-ci.yml", + ".prettierrc", + ".eslintrc", + ".eslintignore", + ".stylelintrc", + ".editorconfig", + ".jshintrc", + ".pylintrc", + ".flake8", + "mypy.ini", + "pyproject.toml", + "tsconfig.json", + "webpack.config.js", + "babel.config.js", + "rollup.config.js", + "jest.config.js", + "karma.conf.js", + "vite.config.js", + "next.config.js", + "*.min.js", + "*.min.css", + "*.bundle.js", + "*.bundle.css", + "*.map", + "*.gz", + "*.zip", + "*.tar", + "*.tgz", + "*.rar", + "*.7z", + "*.iso", + "*.dmg", + "*.img", + "*.msix", + "*.appx", + "*.appxbundle", + "*.xap", + "*.ipa", + "*.deb", + "*.rpm", + "*.msi", + "*.exe", + "*.dll", + "*.so", + "*.dylib", + "*.o", + "*.obj", + "*.jar", + "*.war", + "*.ear", + "*.jsm", + "*.class", + "*.pyc", + "*.pyd", + "*.pyo", + "__pycache__", + "*.a", + "*.lib", + "*.lo", + "*.la", + "*.slo", "*.dSYM", - "*.egg", - "*.egg-info", - "*.dist-info", - "*.eggs", + "*.egg", + "*.egg-info", + "*.dist-info", + "*.eggs", "node_modules", - "bower_components", - "jspm_packages", - "lib-cov", - "coverage", - "htmlcov", - ".nyc_output", - ".tox", - "dist", - "build", - "bld", - "out", - "bin", + "bower_components", + "jspm_packages", + "lib-cov", + "coverage", + "htmlcov", + ".nyc_output", + ".tox", + "dist", + "build", + "bld", + "out", + "bin", "target", - "packages/*/dist", - "packages/*/build", + "packages/*/dist", + "packages/*/build", ".output" ] }, + "code_extensions": [ + ".py", + ".js", + ".ts", + ".java", + ".cpp", + ".c", + ".h", + ".hpp", + ".go", + ".rs", + ".jsx", + ".tsx", + ".html", + ".css", + ".php", + ".swift", + ".cs" + ], + "doc_extensions": [".md", ".txt", ".rst", ".json", ".yaml", ".yml"], "repository": { "max_size_mb": 50000 } diff --git a/api/data_pipeline.py b/api/data_pipeline.py deleted file mode 100644 index 16d15a84d..000000000 --- a/api/data_pipeline.py +++ /dev/null @@ -1,905 +0,0 @@ -import adalflow as adal -from adalflow.core.types import Document, List -from adalflow.components.data_process import TextSplitter, ToEmbeddings -import os -import subprocess -import json -import tiktoken -import logging -import base64 -import glob -from pathlib import Path -from adalflow.utils import get_adalflow_default_root_path -from adalflow.core.db import LocalDB -from api.config import configs, DEFAULT_EXCLUDED_DIRS, DEFAULT_EXCLUDED_FILES -from urllib.parse import urlparse, urlunparse, quote -import requests -from requests.exceptions import RequestException - -from api.tools.embedder import get_embedder - -# Configure logging -logger = logging.getLogger(__name__) - -# Maximum token limit for OpenAI embedding models -MAX_EMBEDDING_TOKENS = 8192 - -def count_tokens(text: str, embedder_type: str = None, is_ollama_embedder: bool = None) -> int: - """ - Count the number of tokens in a text string using tiktoken. - - Args: - text (str): The text to count tokens for. - embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama', 'bedrock'). - If None, will be determined from configuration. - is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. - If None, will be determined from configuration. - - Returns: - int: The number of tokens in the text. - """ - try: - # Handle backward compatibility - if embedder_type is None and is_ollama_embedder is not None: - embedder_type = 'ollama' if is_ollama_embedder else None - - # Determine embedder type if not specified - if embedder_type is None: - from api.config import get_embedder_type - embedder_type = get_embedder_type() - - # Choose encoding based on embedder type - if embedder_type == 'ollama': - # Ollama typically uses cl100k_base encoding - encoding = tiktoken.get_encoding("cl100k_base") - elif embedder_type == 'google': - # Google uses similar tokenization to GPT models for rough estimation - encoding = tiktoken.get_encoding("cl100k_base") - elif embedder_type == 'bedrock': - # Bedrock embedding models vary; use a common GPT-like encoding for rough estimation - encoding = tiktoken.get_encoding("cl100k_base") - else: # OpenAI or default - # Use OpenAI embedding model encoding - encoding = tiktoken.encoding_for_model("text-embedding-3-small") - - return len(encoding.encode(text)) - except Exception as e: - # Fallback to a simple approximation if tiktoken fails - logger.warning(f"Error counting tokens with tiktoken: {e}") - # Rough approximation: 4 characters per token - return len(text) // 4 - -def download_repo(repo_url: str, local_path: str, repo_type: str = None, access_token: str = None) -> str: - """ - Downloads a Git repository (GitHub, GitLab, or Bitbucket) to a specified local path. - - Args: - repo_type(str): Type of repository - repo_url (str): The URL of the Git repository to clone. - local_path (str): The local directory where the repository will be cloned. - access_token (str, optional): Access token for private repositories. - - Returns: - str: The output message from the `git` command. - """ - try: - # Check if Git is installed - logger.info(f"Preparing to clone repository to {local_path}") - subprocess.run( - ["git", "--version"], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - # Check if repository already exists - if os.path.exists(local_path) and os.listdir(local_path): - # Directory exists and is not empty - logger.warning(f"Repository already exists at {local_path}. Using existing repository.") - return f"Using existing repository at {local_path}" - - # Ensure the local path exists - os.makedirs(local_path, exist_ok=True) - - # Prepare the clone URL with access token if provided - clone_url = repo_url - if access_token: - parsed = urlparse(repo_url) - # URL-encode the token to handle special characters - encoded_token = quote(access_token, safe='') - # Determine the repository type and format the URL accordingly - if repo_type == "github": - # Format: https://{token}@{domain}/owner/repo.git - # Works for both github.com and enterprise GitHub domains - clone_url = urlunparse((parsed.scheme, f"{encoded_token}@{parsed.netloc}", parsed.path, '', '', '')) - elif repo_type == "gitlab": - # Format: https://oauth2:{token}@gitlab.com/owner/repo.git - clone_url = urlunparse((parsed.scheme, f"oauth2:{encoded_token}@{parsed.netloc}", parsed.path, '', '', '')) - elif repo_type == "bitbucket": - # Bitbucket has two token formats with different auth schemes: - # - HTTP access tokens (prefix "ATCTT") use x-bitbucket-api-token-auth - # - App passwords (deprecated, EOL June 2026) use x-token-auth - # Detect by token prefix so existing app password users keep working. - if access_token.startswith("ATCTT"): - auth_scheme = "x-bitbucket-api-token-auth" - else: - auth_scheme = "x-token-auth" - # Format: https://{auth_scheme}:{token}@bitbucket.org/owner/repo.git - clone_url = urlunparse((parsed.scheme, f"{auth_scheme}:{encoded_token}@{parsed.netloc}", parsed.path, '', '', '')) - - logger.info("Using access token for authentication") - - # Clone the repository - logger.info(f"Cloning repository from {repo_url} to {local_path}") - # We use repo_url in the log to avoid exposing the token in logs - result = subprocess.run( - ["git", "clone", "--depth=1", "--single-branch", clone_url, local_path], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - logger.info("Repository cloned successfully") - return result.stdout.decode("utf-8") - - except subprocess.CalledProcessError as e: - error_msg = e.stderr.decode('utf-8') - # Sanitize error message to remove any tokens (both raw and URL-encoded) - if access_token: - # Remove raw token - error_msg = error_msg.replace(access_token, "***TOKEN***") - # Also remove URL-encoded token to prevent leaking encoded version - encoded_token = quote(access_token, safe='') - error_msg = error_msg.replace(encoded_token, "***TOKEN***") - raise ValueError(f"Error during cloning: {error_msg}") - except Exception as e: - raise ValueError(f"An unexpected error occurred: {str(e)}") - -# Alias for backward compatibility -download_github_repo = download_repo - -def read_all_documents(path: str, embedder_type: str = None, is_ollama_embedder: bool = None, - excluded_dirs: List[str] = None, excluded_files: List[str] = None, - included_dirs: List[str] = None, included_files: List[str] = None): - """ - Recursively reads all documents in a directory and its subdirectories. - - Args: - path (str): The root directory path. - embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama'). - If None, will be determined from configuration. - is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. - If None, will be determined from configuration. - excluded_dirs (List[str], optional): List of directories to exclude from processing. - Overrides the default configuration if provided. - excluded_files (List[str], optional): List of file patterns to exclude from processing. - Overrides the default configuration if provided. - included_dirs (List[str], optional): List of directories to include exclusively. - When provided, only files in these directories will be processed. - included_files (List[str], optional): List of file patterns to include exclusively. - When provided, only files matching these patterns will be processed. - - Returns: - list: A list of Document objects with metadata. - """ - # Handle backward compatibility - if embedder_type is None and is_ollama_embedder is not None: - embedder_type = 'ollama' if is_ollama_embedder else None - documents = [] - # File extensions to look for, prioritizing code files - code_extensions = [".py", ".js", ".ts", ".java", ".cpp", ".c", ".h", ".hpp", ".go", ".rs", - ".jsx", ".tsx", ".html", ".css", ".php", ".swift", ".cs"] - doc_extensions = [".md", ".txt", ".rst", ".json", ".yaml", ".yml"] - - # Determine filtering mode: inclusion or exclusion - use_inclusion_mode = bool(included_dirs or included_files) - - if use_inclusion_mode: - # Inclusion mode: only process specified directories and files - included_dirs = list(set(included_dirs)) if included_dirs else list() - included_files = list(set(included_files)) if included_files else list() - - logger.info(f"Using inclusion mode") - logger.info(f"Included directories: {included_dirs}") - logger.info(f"Included files: {included_files}") - - # Convert to lists for processing - excluded_dirs = [] - excluded_files = [] - else: - # Exclusion mode: use default exclusions plus any additional ones - final_excluded_dirs = set(DEFAULT_EXCLUDED_DIRS) - final_excluded_files = set(DEFAULT_EXCLUDED_FILES) - - # Add any additional excluded directories from config - if "file_filters" in configs and "excluded_dirs" in configs["file_filters"]: - final_excluded_dirs.update(configs["file_filters"]["excluded_dirs"]) - - # Add any additional excluded files from config - if "file_filters" in configs and "excluded_files" in configs["file_filters"]: - final_excluded_files.update(configs["file_filters"]["excluded_files"]) - - # Add any explicitly provided excluded directories and files - if excluded_dirs is not None: - final_excluded_dirs.update(excluded_dirs) - - if excluded_files is not None: - final_excluded_files.update(excluded_files) - - # Convert back to lists for compatibility - excluded_dirs = list(final_excluded_dirs) - excluded_files = list(final_excluded_files) - included_dirs = [] - included_files = [] - - logger.info(f"Using exclusion mode") - logger.info(f"Excluded directories: {excluded_dirs}") - logger.info(f"Excluded files: {excluded_files}") - - logger.info(f"Reading documents from {path}") - - def should_process_file(file_path: str, use_inclusion: bool, included_dirs: List[str], included_files: List[str], - excluded_dirs: List[str], excluded_files: List[str]) -> bool: - """ - Determine if a file should be processed based on inclusion/exclusion rules. - - Args: - file_path (str): The file path to check - use_inclusion (bool): Whether to use inclusion mode - included_dirs (List[str]): List of directories to include - included_files (List[str]): List of files to include - excluded_dirs (List[str]): List of directories to exclude - excluded_files (List[str]): List of files to exclude - - Returns: - bool: True if the file should be processed, False otherwise - """ - file_path_parts = os.path.normpath(file_path).split(os.sep) - file_name = os.path.basename(file_path) - - if use_inclusion: - # Inclusion mode: file must be in included directories or match included files - is_included = False - - # Check if file is in an included directory - if included_dirs: - for included in included_dirs: - clean_included = included.strip("./").rstrip("/") - if clean_included in file_path_parts: - is_included = True - break - - # Check if file matches included file patterns - if not is_included and included_files: - for included_file in included_files: - if file_name == included_file or file_name.endswith(included_file): - is_included = True - break - - # If no inclusion rules are specified for a category, allow all files from that category - if not included_dirs and not included_files: - is_included = True - elif not included_dirs and included_files: - # Only file patterns specified, allow all directories - pass # is_included is already set based on file patterns - elif included_dirs and not included_files: - # Only directory patterns specified, allow all files in included directories - pass # is_included is already set based on directory patterns - - return is_included - else: - # Exclusion mode: file must not be in excluded directories or match excluded files - is_excluded = False - - # Check if file is in an excluded directory - for excluded in excluded_dirs: - clean_excluded = excluded.strip("./").rstrip("/") - if clean_excluded in file_path_parts: - is_excluded = True - break - - # Check if file matches excluded file patterns - if not is_excluded: - for excluded_file in excluded_files: - if file_name == excluded_file: - is_excluded = True - break - - return not is_excluded - - for file_path in filter( - lambda p: should_process_file( - p, - use_inclusion=use_inclusion_mode, - included_dirs=included_dirs, - included_files=included_files, - excluded_dirs=excluded_dirs, - excluded_files=excluded_files, - ), - filter( - lambda p: p.suffix.lower() in code_extensions + doc_extensions, - Path(path).rglob(pattern="**/*"), - ), - ): - try: - with open(file_path, "r", encoding="utf-8") as f: - content = f.read() - relative_path = os.path.relpath(file_path, path) - - # Check token count - token_count = count_tokens(content, embedder_type) - if token_count > MAX_EMBEDDING_TOKENS * 10: - logger.warning(f"Skipping large file {relative_path}: Token count ({token_count}) exceeds limit") - continue - - file_ext = file_path.suffix.lower() - is_code = file_ext in code_extensions - # Determine if this is an implementation file - if is_code: - is_implementation = ( - not relative_path.startswith("test_") - and not relative_path.startswith("app_") - and "test" not in relative_path.lower() - ) - else: - is_implementation = False - - doc = Document( - text=content, - meta_data={ - "file_path": relative_path, - "type": file_ext, - "is_code": is_code, - "is_implementation": is_implementation, - "title": relative_path, - "token_count": token_count, - }, - ) - documents.append(doc) - except Exception as e: - logger.error(f"Error reading {file_path}: {e}") - - logger.info(f"Found {len(documents)} documents") - return documents - -def prepare_data_pipeline(embedder_type: str = None, is_ollama_embedder: bool = None): - """ - Creates and returns the data transformation pipeline. - - Args: - embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama'). - If None, will be determined from configuration. - is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. - If None, will be determined from configuration. - - Returns: - adal.Sequential: The data transformation pipeline - """ - from api.config import get_embedder_config, get_embedder_type - - # Handle backward compatibility - if embedder_type is None and is_ollama_embedder is not None: - embedder_type = 'ollama' if is_ollama_embedder else None - - # Determine embedder type if not specified - if embedder_type is None: - embedder_type = get_embedder_type() - - splitter = TextSplitter(**configs["text_splitter"]) - embedder_config = get_embedder_config() - - embedder = get_embedder(embedder_type=embedder_type) - - batch_size = embedder_config.get("batch_size", 500) - embedder_transformer = ToEmbeddings( - embedder=embedder, batch_size=batch_size - ) - - data_transformer = adal.Sequential( - splitter, embedder_transformer - ) # sequential will chain together splitter and embedder - return data_transformer - -def transform_documents_and_save_to_db( - documents: List[Document], db_path: str, embedder_type: str = None, is_ollama_embedder: bool = None -) -> LocalDB: - """ - Transforms a list of documents and saves them to a local database. - - Args: - documents (list): A list of `Document` objects. - db_path (str): The path to the local database file. - embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama'). - If None, will be determined from configuration. - is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. - If None, will be determined from configuration. - """ - # Get the data transformer - data_transformer = prepare_data_pipeline(embedder_type, is_ollama_embedder) - - # Save the documents to a local database - db = LocalDB() - db.register_transformer(transformer=data_transformer, key="split_and_embed") - db.load(documents) - db.transform(key="split_and_embed") - os.makedirs(os.path.dirname(db_path), exist_ok=True) - db.save_state(filepath=db_path) - return db - -def get_github_file_content(repo_url: str, file_path: str, access_token: str = None) -> str: - """ - Retrieves the content of a file from a GitHub repository using the GitHub API. - Supports both public GitHub (github.com) and GitHub Enterprise (custom domains). - - Args: - repo_url (str): The URL of the GitHub repository - (e.g., "https://github.com/username/repo" or "https://github.company.com/username/repo") - file_path (str): The path to the file within the repository (e.g., "src/main.py") - access_token (str, optional): GitHub personal access token for private repositories - - Returns: - str: The content of the file as a string - - Raises: - ValueError: If the file cannot be fetched or if the URL is not a valid GitHub URL - """ - try: - # Parse the repository URL to support both github.com and enterprise GitHub - parsed_url = urlparse(repo_url) - if not parsed_url.scheme or not parsed_url.netloc: - raise ValueError("Not a valid GitHub repository URL") - - # Check if it's a GitHub-like URL structure - path_parts = parsed_url.path.strip('/').split('/') - if len(path_parts) < 2: - raise ValueError("Invalid GitHub URL format - expected format: https://domain/owner/repo") - - owner = path_parts[-2] - repo = path_parts[-1].replace(".git", "") - - # Determine the API base URL - if parsed_url.netloc == "github.com": - # Public GitHub - api_base = "https://api.github.com" - else: - # GitHub Enterprise - API is typically at https://domain/api/v3/ - api_base = f"{parsed_url.scheme}://{parsed_url.netloc}/api/v3" - - # Use GitHub API to get file content - # The API endpoint for getting file content is: /repos/{owner}/{repo}/contents/{path} - api_url = f"{api_base}/repos/{owner}/{repo}/contents/{file_path}" - - # Fetch file content from GitHub API - headers = {} - if access_token: - headers["Authorization"] = f"token {access_token}" - logger.info(f"Fetching file content from GitHub API: {api_url}") - try: - response = requests.get(api_url, headers=headers) - response.raise_for_status() - except RequestException as e: - raise ValueError(f"Error fetching file content: {e}") - try: - content_data = response.json() - except json.JSONDecodeError: - raise ValueError("Invalid response from GitHub API") - - # Check if we got an error response - if "message" in content_data and "documentation_url" in content_data: - raise ValueError(f"GitHub API error: {content_data['message']}") - - # GitHub API returns file content as base64 encoded string - if "content" in content_data and "encoding" in content_data: - if content_data["encoding"] == "base64": - # The content might be split into lines, so join them first - content_base64 = content_data["content"].replace("\n", "") - content = base64.b64decode(content_base64).decode("utf-8") - return content - else: - raise ValueError(f"Unexpected encoding: {content_data['encoding']}") - else: - raise ValueError("File content not found in GitHub API response") - - except Exception as e: - raise ValueError(f"Failed to get file content: {str(e)}") - -def get_gitlab_file_content(repo_url: str, file_path: str, access_token: str = None) -> str: - """ - Retrieves the content of a file from a GitLab repository (cloud or self-hosted). - - Args: - repo_url (str): The GitLab repo URL (e.g., "https://gitlab.com/username/repo" or "http://localhost/group/project") - file_path (str): File path within the repository (e.g., "src/main.py") - access_token (str, optional): GitLab personal access token - - Returns: - str: File content - - Raises: - ValueError: If anything fails - """ - try: - # Parse and validate the URL - parsed_url = urlparse(repo_url) - if not parsed_url.scheme or not parsed_url.netloc: - raise ValueError("Not a valid GitLab repository URL") - - gitlab_domain = f"{parsed_url.scheme}://{parsed_url.netloc}" - if parsed_url.port not in (None, 80, 443): - gitlab_domain += f":{parsed_url.port}" - path_parts = parsed_url.path.strip("/").split("/") - if len(path_parts) < 2: - raise ValueError("Invalid GitLab URL format — expected something like https://gitlab.domain.com/group/project") - - # Build project path and encode for API - project_path = "/".join(path_parts).replace(".git", "") - encoded_project_path = quote(project_path, safe='') - - # Encode file path - encoded_file_path = quote(file_path, safe='') - - # Try to get the default branch from the project info - default_branch = None - try: - project_info_url = f"{gitlab_domain}/api/v4/projects/{encoded_project_path}" - project_headers = {} - if access_token: - project_headers["PRIVATE-TOKEN"] = access_token - - project_response = requests.get(project_info_url, headers=project_headers) - if project_response.status_code == 200: - project_data = project_response.json() - default_branch = project_data.get('default_branch', 'main') - logger.info(f"Found default branch: {default_branch}") - else: - logger.warning(f"Could not fetch project info, using 'main' as default branch") - default_branch = 'main' - except Exception as e: - logger.warning(f"Error fetching project info: {e}, using 'main' as default branch") - default_branch = 'main' - - api_url = f"{gitlab_domain}/api/v4/projects/{encoded_project_path}/repository/files/{encoded_file_path}/raw?ref={default_branch}" - # Fetch file content from GitLab API - headers = {} - if access_token: - headers["PRIVATE-TOKEN"] = access_token - logger.info(f"Fetching file content from GitLab API: {api_url}") - try: - response = requests.get(api_url, headers=headers) - response.raise_for_status() - content = response.text - except RequestException as e: - raise ValueError(f"Error fetching file content: {e}") - - # Check for GitLab error response (JSON instead of raw file) - if content.startswith("{") and '"message":' in content: - try: - error_data = json.loads(content) - if "message" in error_data: - raise ValueError(f"GitLab API error: {error_data['message']}") - except json.JSONDecodeError: - pass - - return content - - except Exception as e: - raise ValueError(f"Failed to get file content: {str(e)}") - -def get_bitbucket_file_content(repo_url: str, file_path: str, access_token: str = None) -> str: - """ - Retrieves the content of a file from a Bitbucket repository using the Bitbucket API. - - Args: - repo_url (str): The URL of the Bitbucket repository (e.g., "https://bitbucket.org/username/repo") - file_path (str): The path to the file within the repository (e.g., "src/main.py") - access_token (str, optional): Bitbucket personal access token for private repositories - - Returns: - str: The content of the file as a string - """ - try: - # Extract owner and repo name from Bitbucket URL - if not (repo_url.startswith("https://bitbucket.org/") or repo_url.startswith("http://bitbucket.org/")): - raise ValueError("Not a valid Bitbucket repository URL") - - parts = repo_url.rstrip('/').split('/') - if len(parts) < 5: - raise ValueError("Invalid Bitbucket URL format") - - owner = parts[-2] - repo = parts[-1].replace(".git", "") - - # Try to get the default branch from the repository info - default_branch = None - try: - repo_info_url = f"https://api.bitbucket.org/2.0/repositories/{owner}/{repo}" - repo_headers = {} - if access_token: - repo_headers["Authorization"] = f"Bearer {access_token}" - - repo_response = requests.get(repo_info_url, headers=repo_headers) - if repo_response.status_code == 200: - repo_data = repo_response.json() - default_branch = repo_data.get('mainbranch', {}).get('name', 'main') - logger.info(f"Found default branch: {default_branch}") - else: - logger.warning(f"Could not fetch repository info, using 'main' as default branch") - default_branch = 'main' - except Exception as e: - logger.warning(f"Error fetching repository info: {e}, using 'main' as default branch") - default_branch = 'main' - - # Use Bitbucket API to get file content - # The API endpoint for getting file content is: /2.0/repositories/{owner}/{repo}/src/{branch}/{path} - api_url = f"https://api.bitbucket.org/2.0/repositories/{owner}/{repo}/src/{default_branch}/{file_path}" - - # Fetch file content from Bitbucket API - headers = {} - if access_token: - headers["Authorization"] = f"Bearer {access_token}" - logger.info(f"Fetching file content from Bitbucket API: {api_url}") - try: - response = requests.get(api_url, headers=headers) - if response.status_code == 200: - content = response.text - elif response.status_code == 404: - raise ValueError("File not found on Bitbucket. Please check the file path and repository.") - elif response.status_code == 401: - raise ValueError("Unauthorized access to Bitbucket. Please check your access token.") - elif response.status_code == 403: - raise ValueError("Forbidden access to Bitbucket. You might not have permission to access this file.") - elif response.status_code == 500: - raise ValueError("Internal server error on Bitbucket. Please try again later.") - else: - response.raise_for_status() - content = response.text - return content - except RequestException as e: - raise ValueError(f"Error fetching file content: {e}") - - except Exception as e: - raise ValueError(f"Failed to get file content: {str(e)}") - - -def get_file_content(repo_url: str, file_path: str, repo_type: str = None, access_token: str = None) -> str: - """ - Retrieves the content of a file from a Git repository (GitHub or GitLab). - - Args: - repo_type (str): Type of repository - repo_url (str): The URL of the repository - file_path (str): The path to the file within the repository - access_token (str, optional): Access token for private repositories - - Returns: - str: The content of the file as a string - - Raises: - ValueError: If the file cannot be fetched or if the URL is not valid - """ - if repo_type == "github": - return get_github_file_content(repo_url, file_path, access_token) - elif repo_type == "gitlab": - return get_gitlab_file_content(repo_url, file_path, access_token) - elif repo_type == "bitbucket": - return get_bitbucket_file_content(repo_url, file_path, access_token) - else: - raise ValueError("Unsupported repository type. Only GitHub, GitLab, and Bitbucket are supported.") - -class DatabaseManager: - """ - Manages the creation, loading, transformation, and persistence of LocalDB instances. - """ - - def __init__(self): - self.db = None - self.repo_url_or_path = None - self.repo_paths = None - - def prepare_database(self, repo_url_or_path: str, repo_type: str = None, access_token: str = None, - embedder_type: str = None, is_ollama_embedder: bool = None, - excluded_dirs: List[str] = None, excluded_files: List[str] = None, - included_dirs: List[str] = None, included_files: List[str] = None) -> List[Document]: - """ - Create a new database from the repository. - - Args: - repo_type(str): Type of repository - repo_url_or_path (str): The URL or local path of the repository - access_token (str, optional): Access token for private repositories - embedder_type (str, optional): Embedder type to use ('openai', 'google', 'ollama'). - If None, will be determined from configuration. - is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. - If None, will be determined from configuration. - excluded_dirs (List[str], optional): List of directories to exclude from processing - excluded_files (List[str], optional): List of file patterns to exclude from processing - included_dirs (List[str], optional): List of directories to include exclusively - included_files (List[str], optional): List of file patterns to include exclusively - - Returns: - List[Document]: List of Document objects - """ - # Handle backward compatibility - if embedder_type is None and is_ollama_embedder is not None: - embedder_type = 'ollama' if is_ollama_embedder else None - - self.reset_database() - self._create_repo(repo_url_or_path, repo_type, access_token) - return self.prepare_db_index(embedder_type=embedder_type, excluded_dirs=excluded_dirs, excluded_files=excluded_files, - included_dirs=included_dirs, included_files=included_files) - - def reset_database(self): - """ - Reset the database to its initial state. - """ - self.db = None - self.repo_url_or_path = None - self.repo_paths = None - - def _extract_repo_name_from_url(self, repo_url_or_path: str, repo_type: str) -> str: - # Extract owner and repo name to create unique identifier - url_parts = repo_url_or_path.rstrip('/').split('/') - - if repo_type in ["github", "gitlab", "bitbucket"] and len(url_parts) >= 5: - # GitHub URL format: https://github.com/owner/repo - # GitLab URL format: https://gitlab.com/owner/repo or https://gitlab.com/group/subgroup/repo - # Bitbucket URL format: https://bitbucket.org/owner/repo - owner = url_parts[-2] - repo = url_parts[-1].replace(".git", "") - repo_name = f"{owner}_{repo}" - else: - repo_name = url_parts[-1].replace(".git", "") - return repo_name - - def _create_repo(self, repo_url_or_path: str, repo_type: str = None, access_token: str = None) -> None: - """ - Download and prepare all paths. - Paths: - ~/.adalflow/repos/{owner}_{repo_name} (for url, local path will be the same) - ~/.adalflow/databases/{owner}_{repo_name}.pkl - - Args: - repo_type(str): Type of repository - repo_url_or_path (str): The URL or local path of the repository - access_token (str, optional): Access token for private repositories - """ - logger.info(f"Preparing repo storage for {repo_url_or_path}...") - - try: - # Strip whitespace to handle URLs with leading/trailing spaces - repo_url_or_path = repo_url_or_path.strip() - - root_path = get_adalflow_default_root_path() - - os.makedirs(root_path, exist_ok=True) - # url - if repo_url_or_path.startswith("https://") or repo_url_or_path.startswith("http://"): - # Extract the repository name from the URL - repo_name = self._extract_repo_name_from_url(repo_url_or_path, repo_type) - logger.info(f"Extracted repo name: {repo_name}") - - save_repo_dir = os.path.join(root_path, "repos", repo_name) - - # Check if the repository directory already exists and is not empty - if not (os.path.exists(save_repo_dir) and os.listdir(save_repo_dir)): - # Only download if the repository doesn't exist or is empty - download_repo(repo_url_or_path, save_repo_dir, repo_type, access_token) - else: - logger.info(f"Repository already exists at {save_repo_dir}. Using existing repository.") - else: # local path - repo_name = os.path.basename(repo_url_or_path) - save_repo_dir = repo_url_or_path - - save_db_file = os.path.join(root_path, "databases", f"{repo_name}.pkl") - os.makedirs(save_repo_dir, exist_ok=True) - os.makedirs(os.path.dirname(save_db_file), exist_ok=True) - - self.repo_paths = { - "save_repo_dir": save_repo_dir, - "save_db_file": save_db_file, - } - self.repo_url_or_path = repo_url_or_path - logger.info(f"Repo paths: {self.repo_paths}") - - except Exception as e: - logger.error(f"Failed to create repository structure: {e}") - raise - - def prepare_db_index(self, embedder_type: str = None, is_ollama_embedder: bool = None, - excluded_dirs: List[str] = None, excluded_files: List[str] = None, - included_dirs: List[str] = None, included_files: List[str] = None) -> List[Document]: - """ - Prepare the indexed database for the repository. - - Args: - embedder_type (str, optional): Embedder type to use ('openai', 'google', 'ollama'). - If None, will be determined from configuration. - is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. - If None, will be determined from configuration. - excluded_dirs (List[str], optional): List of directories to exclude from processing - excluded_files (List[str], optional): List of file patterns to exclude from processing - included_dirs (List[str], optional): List of directories to include exclusively - included_files (List[str], optional): List of file patterns to include exclusively - - Returns: - List[Document]: List of Document objects - """ - def _embedding_vector_length(doc: Document) -> int: - vector = getattr(doc, "vector", None) - if vector is None: - return 0 - try: - if hasattr(vector, "shape"): - if len(vector.shape) == 0: - return 0 - return int(vector.shape[-1]) - if hasattr(vector, "__len__"): - return int(len(vector)) - except Exception: - return 0 - return 0 - - # Handle backward compatibility - if embedder_type is None and is_ollama_embedder is not None: - embedder_type = 'ollama' if is_ollama_embedder else None - # check the database - if self.repo_paths and os.path.exists(self.repo_paths["save_db_file"]): - logger.info("Loading existing database...") - try: - self.db = LocalDB.load_state(self.repo_paths["save_db_file"]) - documents = self.db.get_transformed_data(key="split_and_embed") - if documents: - lengths = [_embedding_vector_length(doc) for doc in documents] - non_empty = sum(1 for n in lengths if n > 0) - empty = len(lengths) - non_empty - sample_sizes = sorted({n for n in lengths if n > 0})[:3] - logger.info( - "Loaded %s documents from existing database (embeddings: %s non-empty, %s empty; sample_dims=%s)", - len(documents), - non_empty, - empty, - sample_sizes, - ) - - if non_empty == 0: - logger.warning( - "Existing database contains no usable embeddings. Rebuilding embeddings..." - ) - else: - return documents - except Exception as e: - logger.error(f"Error loading existing database: {e}") - # Continue to create a new database - - # prepare the database - logger.info("Creating new database...") - documents = read_all_documents( - self.repo_paths["save_repo_dir"], - embedder_type=embedder_type, - excluded_dirs=excluded_dirs, - excluded_files=excluded_files, - included_dirs=included_dirs, - included_files=included_files - ) - self.db = transform_documents_and_save_to_db( - documents, self.repo_paths["save_db_file"], embedder_type=embedder_type - ) - logger.info(f"Total documents: {len(documents)}") - transformed_docs = self.db.get_transformed_data(key="split_and_embed") - logger.info(f"Total transformed documents: {len(transformed_docs)}") - return transformed_docs - - def prepare_retriever(self, repo_url_or_path: str, repo_type: str = None, access_token: str = None): - """ - Prepare the retriever for a repository. - This is a compatibility method for the isolated API. - - Args: - repo_type(str): Type of repository - repo_url_or_path (str): The URL or local path of the repository - access_token (str, optional): Access token for private repositories - - Returns: - List[Document]: List of Document objects - """ - return self.prepare_database(repo_url_or_path, repo_type, access_token) diff --git a/api/logger.py b/api/logger.py new file mode 100644 index 000000000..52b289f16 --- /dev/null +++ b/api/logger.py @@ -0,0 +1,107 @@ +import json +import logging +import logging.config +import os +from pathlib import Path +from typing import Any + +__all__ = ["get_logger", "setup_logging"] + + +def get_logger(name: str | None = None) -> logging.Logger: + """Get a logger instance with a specific name in `deepwiki` namespace. + + Parameters + ---------- + name: Optional[str] + The name of the logger. default=None. + + Returns + ------- + logging.Logger + A logger instance with the specific name. + + """ + logger = logging.getLogger("deepwiki") + if name: + logger = logger.getChild(name) + + return logger + + +def _default_log_config( + path: str | None = None, + max_bytes: int = 10485760, # 10MB + backup_count: int = 5, +) -> dict[str, Any]: + handlers: dict[str, dict[str, str | int]] = { + "stdout": { + "class": "logging.StreamHandler", + "level": "INFO", + "formatter": "default", + "stream": "ext://sys.stdout", + }, + } + if path is not None: + log_dir = os.path.dirname(path) + if log_dir: + os.makedirs(log_dir, exist_ok=True) + + handlers["ps_file"] = { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "default", + "filename": path, + "maxBytes": max_bytes, + "backupCount": backup_count, + "encoding": "utf-8", + } + loggers = { + "deepwiki": { + "handlers": list(handlers.keys()), + "level": "INFO", + "propagate": False, + }, + } + + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "format": "%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s", + }, + }, + "handlers": handlers, + "loggers": loggers, + } + + +def _ensure_handler_dirs(log_cfg: dict[str, Any]) -> None: + """Create parent directories for any file-based handlers in the config.""" + for handler in log_cfg.get("handlers", {}).values(): + if filename := handler.get("filename"): + log_dir = os.path.dirname(filename) + if log_dir: + os.makedirs(log_dir, exist_ok=True) + + +def setup_logging() -> None: + cfg_path = os.path.join(os.getcwd(), "log_cfg.json") + if os.path.isfile(cfg_path): + print(f"loading config from {cfg_path}") + with open(cfg_path, "r") as f: + log_cfg = json.load(f) + _ensure_handler_dirs(log_cfg) + else: + log_file = os.getenv( + "LOG_FILE_PATH", + (Path(__file__).parent / "logs" / "application.log").as_posix(), + ) + log_cfg = _default_log_config( + log_file, + max_bytes=int(os.getenv("LOG_MAX_SIZE", "10")) * 1024 * 1024, + backup_count=int(os.getenv("LOG_BACKUP_COUNT", "5")), + ) + + logging.config.dictConfig(log_cfg) diff --git a/api/logging_config.py b/api/logging_config.py deleted file mode 100644 index d2726c626..000000000 --- a/api/logging_config.py +++ /dev/null @@ -1,85 +0,0 @@ -import logging -import os -from pathlib import Path -from logging.handlers import RotatingFileHandler - - -class IgnoreLogChangeDetectedFilter(logging.Filter): - def filter(self, record: logging.LogRecord): - return "Detected file change in" not in record.getMessage() - - -def setup_logging(format: str = None): - """ - Configure logging for the application with log rotation. - - Environment variables: - LOG_LEVEL: Log level (default: INFO) - LOG_FILE_PATH: Path to log file (default: logs/application.log) - LOG_MAX_SIZE: Max size in MB before rotating (default: 10MB) - LOG_BACKUP_COUNT: Number of backup files to keep (default: 5) - - Ensures log directory exists, prevents path traversal, and configures - both rotating file and console handlers. - """ - # Determine log directory and default file path - base_dir = Path(__file__).parent - log_dir = base_dir / "logs" - log_dir.mkdir(parents=True, exist_ok=True) - default_log_file = log_dir / "application.log" - - # Get log level from environment - log_level_str = os.environ.get("LOG_LEVEL", "INFO").upper() - log_level = getattr(logging, log_level_str, logging.INFO) - - # Get log file path - log_file_path = Path(os.environ.get("LOG_FILE_PATH", str(default_log_file))) - - # Secure path check: must be inside logs/ directory - log_dir_resolved = log_dir.resolve() - resolved_path = log_file_path.resolve() - if not str(resolved_path).startswith(str(log_dir_resolved) + os.sep): - raise ValueError(f"LOG_FILE_PATH '{log_file_path}' is outside the trusted log directory '{log_dir_resolved}'") - - # Ensure parent directories exist - resolved_path.parent.mkdir(parents=True, exist_ok=True) - - # Get max log file size (default: 10MB) - try: - max_mb = int(os.environ.get("LOG_MAX_SIZE", 10)) # 10MB default - max_bytes = max_mb * 1024 * 1024 - except (TypeError, ValueError): - max_bytes = 10 * 1024 * 1024 # fallback to 10MB on error - - # Get backup count (default: 5) - try: - backup_count = int(os.environ.get("LOG_BACKUP_COUNT", 5)) - except ValueError: - backup_count = 5 - - # Configure format - log_format = format or "%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s" - - # Create handlers - file_handler = RotatingFileHandler(resolved_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8") - console_handler = logging.StreamHandler() - - # Set format for both handlers - formatter = logging.Formatter(log_format) - file_handler.setFormatter(formatter) - console_handler.setFormatter(formatter) - - # Add filter to suppress "Detected file change" messages - file_handler.addFilter(IgnoreLogChangeDetectedFilter()) - console_handler.addFilter(IgnoreLogChangeDetectedFilter()) - - # Apply logging configuration - logging.basicConfig(level=log_level, handlers=[file_handler, console_handler], force=True) - - # Log configuration info - logger = logging.getLogger(__name__) - logger.debug( - f"Logging configured: level={log_level_str}, " - f"file={resolved_path}, max_size={max_bytes} bytes, " - f"backup_count={backup_count}" - ) diff --git a/api/main.py b/api/main.py index fe083f550..1b024874d 100644 --- a/api/main.py +++ b/api/main.py @@ -1,32 +1,37 @@ -import os -import sys import logging +import os + from dotenv import load_dotenv +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +import uvicorn # Load environment variables from .env file +# ruff: noqa: E402 load_dotenv() +# ruff: noqa: E402 -from api.logging_config import setup_logging +from api.logger import get_logger, setup_logging +from api.routers import auth, chat, codemap, repo, system, wiki # Configure logging setup_logging() -logger = logging.getLogger(__name__) +logger = get_logger(__name__) # Configure watchfiles logger to show file paths watchfiles_logger = logging.getLogger("watchfiles.main") watchfiles_logger.setLevel(logging.DEBUG) # Enable DEBUG to see file paths -# Add the current directory to the path so we can import the api package -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - # Apply watchfiles monkey patch BEFORE uvicorn import is_development = os.environ.get("NODE_ENV") != "production" if is_development: import watchfiles + current_dir = os.path.dirname(os.path.abspath(__file__)) logs_dir = os.path.join(current_dir, "logs") - + original_watch = watchfiles.watch + def patched_watch(*args, **kwargs): # Only watch the api directory but exclude logs subdirectory # Instead of watching the entire api directory, watch specific subdirectories @@ -37,42 +42,77 @@ def patched_watch(*args, **kwargs): api_subdirs.append(item_path) elif os.path.isfile(item_path) and item.endswith(".py"): api_subdirs.append(item_path) - + return original_watch(*api_subdirs, **kwargs) + watchfiles.watch = patched_watch -import uvicorn +app = FastAPI( + title="Streaming API", + description="API for streaming chat completions and wiki generation", + version="0.1.0", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) -# Check for required environment variables -required_env_vars = ['GOOGLE_API_KEY', 'OPENAI_API_KEY'] -missing_vars = [var for var in required_env_vars if not os.environ.get(var)] -if missing_vars: - logger.warning(f"Missing environment variables: {', '.join(missing_vars)}") - logger.warning("Some functionality may not work correctly without these variables.") +for module in ( + system, + auth, + repo, + wiki, + chat, + codemap, +): + app.include_router(module.router) -# Configure Google Generative AI -import google.generativeai as genai -from api.config import GOOGLE_API_KEY -if GOOGLE_API_KEY: - genai.configure(api_key=GOOGLE_API_KEY) -else: - logger.warning("GOOGLE_API_KEY not configured") +@app.get("/") +async def root(): + """Root endpoint to check if the API is running and list available endpoints dynamically.""" + # Collect routes dynamically from the FastAPI app + endpoints = {} + for route in app.routes: + if hasattr(route, "methods") and hasattr(route, "path"): + # Skip docs and static routes + if route.path in ["/openapi.json", "/docs", "/redoc", "/favicon.ico"]: + continue + # Group endpoints by first path segment + path_parts = route.path.strip("/").split("/") + group = path_parts[0].capitalize() if path_parts[0] else "Root" + method_list = list(route.methods - {"HEAD", "OPTIONS"}) + for method in method_list: + endpoints.setdefault(group, []).append(f"{method} {route.path}") + + # Optionally, sort endpoints for readability + for group in endpoints: + endpoints[group].sort() + + return { + "message": "Welcome to Streaming API", + "version": "1.0.0", + "endpoints": endpoints, + } + if __name__ == "__main__": # Get port from environment variable or use default port = int(os.environ.get("PORT", 8001)) - # Import the app here to ensure environment variables are set first - from api.api import app - logger.info(f"Starting Streaming API on port {port}") # Run the FastAPI app with uvicorn uvicorn.run( - "api.api:app", + "api.main:app", host="0.0.0.0", port=port, reload=is_development, - reload_excludes=["**/logs/*", "**/__pycache__/*", "**/*.pyc"] if is_development else None, + reload_excludes=["**/logs/*", "**/__pycache__/*", "**/*.pyc"] + if is_development + else None, ) diff --git a/api/ollama_patch.py b/api/ollama_patch.py deleted file mode 100644 index cc04b7317..000000000 --- a/api/ollama_patch.py +++ /dev/null @@ -1,49 +0,0 @@ -import logging - -import os - -# Configure logging -from api.logging_config import setup_logging - -setup_logging() -logger = logging.getLogger(__name__) - -class OllamaModelNotFoundError(Exception): - """Custom exception for when Ollama model is not found""" - pass - -def check_ollama_model_exists(model_name: str, ollama_host: str = None) -> bool: - """ - Check if an Ollama model exists before attempting to use it. - - Args: - model_name: Name of the model to check - ollama_host: Ollama host URL, defaults to localhost:11434 - - Returns: - bool: True if model exists, False otherwise - """ - import ollama - import httpx - if ollama_host is None: - ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434") - try: - # Remove /api prefix if present and add it back - if ollama_host.endswith('/api'): - ollama_host = ollama_host[:-4] - ret: ollama.ListResponse = ollama.Client(host=ollama_host, timeout=5).list() - is_available = any(model_name == model.model for model in ret.models) - if is_available: - logger.info("Ollama model '%s' is available", model_name) - else: - logger.warning( - "Ollama model '%s' is not available. Available models: %s. ", - model_name, - str([model.model for model in ret.models])) - return is_available - except (httpx.ConnectTimeout, ConnectionError) as e: - logger.warning(f"Could not connect to Ollama to check models: {e}") - return False - except Exception as e: - logger.warning(f"Error checking Ollama model availability: {e}") - return False diff --git a/api/poetry.lock b/api/poetry.lock index afc9b1de9..153ecd4a5 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -227,6 +227,37 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "anthropic" +version = "0.117.1" +description = "The official Python library for the anthropic API" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "anthropic-0.117.1-py3-none-any.whl", hash = "sha256:7ab79e2c33c3dbde54c5800c4d535821e1c6d80bdf7de83ae1c0328bae563b76"}, + {file = "anthropic-0.117.1.tar.gz", hash = "sha256:e015a2d5b99fdf0aecac246e24ddb40d7bf93f9fe5b198771fab1559fb0101b9"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +docstring-parser = ">=0.15,<1" +httpx = ">=0.25.0,<1" +jiter = ">=0.4.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +typing-extensions = ">=4.14,<5" + +[package.extras] +aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9,<1)"] +aws = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] +bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] +google-cloud = ["google-auth[requests] (>=2,<3)"] +mcp = ["mcp (>=1.0) ; python_version >= \"3.10\""] +vertex = ["google-auth[requests] (>=2,<3)"] +webhooks = ["standardwebhooks (>=1.0.1,<2)"] + [[package]] name = "anyio" version = "4.11.0" @@ -741,6 +772,23 @@ files = [ {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, ] +[[package]] +name = "docstring-parser" +version = "0.18.0" +description = "Parse Python docstrings in reST, Google and Numpydoc format" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b"}, + {file = "docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015"}, +] + +[package.extras] +dev = ["pre-commit (>=2.16.0) ; python_version >= \"3.9\"", "pydoctor (>=25.4.0)", "pytest"] +docs = ["pydoctor (>=25.4.0)"] +test = ["pytest"] + [[package]] name = "faiss-cpu" version = "1.11.0.post1" @@ -3404,4 +3452,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "6ba5b45f26d528b886a8c74acebcdc4241bb7ff19474c040244f446bb6d9e784" +content-hash = "86ee62820be3180e14bcd1bb48607994e24ba6648e13a346b8cee26fcdc89a06" diff --git a/api/prompts.py b/api/prompts.py index 7c6641553..f126fa128 100644 --- a/api/prompts.py +++ b/api/prompts.py @@ -189,3 +189,77 @@ - When showing code, include line numbers and file paths when relevant - Use markdown formatting to improve readability """ + + +# Codemap generation — phase 1: analyze the code and produce the codemap skeleton. +CODEMAP_SKELETON_PROMPT = """ +You are an expert code analyst building a "codemap" for the {repo_type} repository: {repo_url} ({repo_name}). +A codemap is a structured, step-by-step guide that answers a usage/how-to question, where every +step is grounded in REAL source code from the repository. +IMPORTANT: All human-readable text (titles, labels, summary) MUST be written in {language_name} language. + + + +Using ONLY the code provided in ..., produce a codemap that +answers the user's question. Organise the answer as numbered sections (1, 2, 3...), each containing +ordered sub-steps (1a, 1b, 1c...). Every sub-step must reference the real source it comes from. + + + +- You may ONLY cite files that appear in the context as a "## File Path: " header. Never invent a path. +- Each chunk in the context is prefixed with a "[lines A-B]" marker. Use those numbers to fill start_line/end_line. +- The "snippet" field MUST be copied VERBATIM from the context (an exact substring). Do not paraphrase it. +- The "code" field is a short example snippet illustrating the step; keep it minimal and runnable-looking. +- If the context does not contain enough to answer, produce fewer sections rather than fabricating. + + + +Output ONLY a single JSON object, no markdown fences, no commentary before or after. Shape: +{{ + "title": "", + "summary": "<1-3 sentence intro; you may reference steps like [1a] [2a]>", + "sections": [ + {{ + "id": "1", + "title": "
", + "guide": "", + "diagram": "", + "steps": [ + {{ + "id": "1a", + "label": "", + "code": "", + "citation": {{ + "file_path": "", + "start_line": , + "end_line": , + "snippet": "" + }} + }} + ] + }} + ] +}} +Leave every "guide" and "diagram" field as an empty string "" in this phase. +""" + + +# Codemap generation — phase 2: fill in prose guides and mermaid diagrams. +CODEMAP_ENRICH_PROMPT = """ +You are enriching an existing codemap skeleton for the {repo_type} repository: {repo_url} ({repo_name}). +IMPORTANT: All prose MUST be written in {language_name} language. + + + +You are given a codemap JSON skeleton (in ) and the original source context. +For EACH section, write: +- "guide": a concise prose explanation (2-4 sentences) of what the section accomplishes. +- "diagram": a valid Mermaid diagram source string (e.g. a "graph LR" or "flowchart TD") that + illustrates the flow of that section. Use only Mermaid syntax; do NOT wrap it in ```mermaid fences. +Keep every other field (title, summary, steps, citations, ids) EXACTLY as given. Do not add or remove steps. + + + +Output ONLY the complete updated JSON object with the same shape as the skeleton, now with +"guide" and "diagram" filled for each section. No markdown fences, no commentary. +""" diff --git a/api/pyproject.toml b/api/pyproject.toml index 1dcdef7dc..ad1f5d32c 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -28,6 +28,7 @@ boto3 = ">=1.34.0" websockets = ">=11.0.3" azure-identity = ">=1.12.0" azure-core = ">=1.24.0" +anthropic = "^0.117.1" [build-system] diff --git a/api/rag.py b/api/rag.py deleted file mode 100644 index 157ed3460..000000000 --- a/api/rag.py +++ /dev/null @@ -1,449 +0,0 @@ -import logging -import asyncio -import os -from dataclasses import dataclass -from typing import Any, List, Tuple, Dict -from uuid import uuid4 - -import adalflow as adal - -from api.tools.embedder import get_embedder -from api.prompts import RAG_SYSTEM_PROMPT as system_prompt, RAG_TEMPLATE - -# Create our own implementation of the conversation classes -@dataclass -class UserQuery: - query_str: str - -@dataclass -class AssistantResponse: - response_str: str - -@dataclass -class DialogTurn: - id: str - user_query: UserQuery - assistant_response: AssistantResponse - -class CustomConversation: - """Custom implementation of Conversation to fix the list assignment index out of range error""" - - def __init__(self): - self.dialog_turns = [] - - def append_dialog_turn(self, dialog_turn): - """Safely append a dialog turn to the conversation""" - if not hasattr(self, 'dialog_turns'): - self.dialog_turns = [] - self.dialog_turns.append(dialog_turn) - -# Import other adalflow components -from adalflow.components.retriever.faiss_retriever import FAISSRetriever -from api.config import configs -from api.data_pipeline import DatabaseManager - -# Configure logging -logger = logging.getLogger(__name__) - -# Maximum token limit for embedding models -MAX_INPUT_TOKENS = 7500 # Safe threshold below 8192 token limit - -# Maximum concurrent RAG preparing count -_RAG_PREPARE_SEMAPHORE: asyncio.Semaphore | None = None - - -def _get_rag_semaphore() -> asyncio.Semaphore: - global _RAG_PREPARE_SEMAPHORE - if _RAG_PREPARE_SEMAPHORE is None: - _RAG_PREPARE_SEMAPHORE = asyncio.Semaphore( - int(os.environ.get("DEEPWIKI_MAX_CONCURRENT_RAG", "4")) - ) - assert isinstance(_RAG_PREPARE_SEMAPHORE, asyncio.Semaphore) - return _RAG_PREPARE_SEMAPHORE - - -class Memory(adal.core.component.DataComponent): - """Simple conversation management with a list of dialog turns.""" - - def __init__(self): - super().__init__() - # Use our custom implementation instead of the original Conversation class - self.current_conversation = CustomConversation() - - def call(self) -> Dict: - """Return the conversation history as a dictionary.""" - all_dialog_turns = {} - try: - # Check if dialog_turns exists and is a list - if hasattr(self.current_conversation, 'dialog_turns'): - if self.current_conversation.dialog_turns: - logger.info(f"Memory content: {len(self.current_conversation.dialog_turns)} turns") - for i, turn in enumerate(self.current_conversation.dialog_turns): - if hasattr(turn, 'id') and turn.id is not None: - all_dialog_turns[turn.id] = turn - logger.info(f"Added turn {i+1} with ID {turn.id} to memory") - else: - logger.warning(f"Skipping invalid turn object in memory: {turn}") - else: - logger.info("Dialog turns list exists but is empty") - else: - logger.info("No dialog_turns attribute in current_conversation") - # Try to initialize it - self.current_conversation.dialog_turns = [] - except Exception as e: - logger.error(f"Error accessing dialog turns: {str(e)}") - # Try to recover - try: - self.current_conversation = CustomConversation() - logger.info("Recovered by creating new conversation") - except Exception as e2: - logger.error(f"Failed to recover: {str(e2)}") - - logger.info(f"Returning {len(all_dialog_turns)} dialog turns from memory") - return all_dialog_turns - - def add_dialog_turn(self, user_query: str, assistant_response: str) -> bool: - """ - Add a dialog turn to the conversation history. - - Args: - user_query: The user's query - assistant_response: The assistant's response - - Returns: - bool: True if successful, False otherwise - """ - try: - # Create a new dialog turn using our custom implementation - dialog_turn = DialogTurn( - id=str(uuid4()), - user_query=UserQuery(query_str=user_query), - assistant_response=AssistantResponse(response_str=assistant_response), - ) - - # Make sure the current_conversation has the append_dialog_turn method - if not hasattr(self.current_conversation, 'append_dialog_turn'): - logger.warning("current_conversation does not have append_dialog_turn method, creating new one") - # Initialize a new conversation if needed - self.current_conversation = CustomConversation() - - # Ensure dialog_turns exists - if not hasattr(self.current_conversation, 'dialog_turns'): - logger.warning("dialog_turns not found, initializing empty list") - self.current_conversation.dialog_turns = [] - - # Safely append the dialog turn - self.current_conversation.dialog_turns.append(dialog_turn) - logger.info(f"Successfully added dialog turn, now have {len(self.current_conversation.dialog_turns)} turns") - return True - - except Exception as e: - logger.error(f"Error adding dialog turn: {str(e)}") - # Try to recover by creating a new conversation - try: - self.current_conversation = CustomConversation() - dialog_turn = DialogTurn( - id=str(uuid4()), - user_query=UserQuery(query_str=user_query), - assistant_response=AssistantResponse(response_str=assistant_response), - ) - self.current_conversation.dialog_turns.append(dialog_turn) - logger.info("Recovered from error by creating new conversation") - return True - except Exception as e2: - logger.error(f"Failed to recover from error: {str(e2)}") - return False - - -from dataclasses import dataclass, field - -@dataclass -class RAGAnswer(adal.DataClass): - rationale: str = field(default="", metadata={"desc": "Chain of thoughts for the answer."}) - answer: str = field(default="", metadata={"desc": "Answer to the user query, formatted in markdown for beautiful rendering with react-markdown. DO NOT include ``` triple backticks fences at the beginning or end of your answer."}) - - __output_fields__ = ["rationale", "answer"] - -class RAG(adal.Component): - """RAG with one repo. - If you want to load a new repos, call prepare_retriever(repo_url_or_path) first.""" - - def __init__(self, provider="google", model=None, use_s3: bool = False): # noqa: F841 - use_s3 is kept for compatibility - """ - Initialize the RAG component. - - Args: - provider: Model provider to use (google, openai, openrouter, ollama) - model: Model name to use with the provider - use_s3: Whether to use S3 for database storage (default: False) - """ - super().__init__() - - self.provider = provider - self.model = model - - # Import the helper functions - from api.config import get_embedder_config, get_embedder_type - - # Determine embedder type based on current configuration - self.embedder_type = get_embedder_type() - self.is_ollama_embedder = (self.embedder_type == 'ollama') # Backward compatibility - - # Check if Ollama model exists before proceeding - if self.is_ollama_embedder: - from api.ollama_patch import check_ollama_model_exists - from api.config import get_embedder_config - - embedder_config = get_embedder_config() - if embedder_config and embedder_config.get("model_kwargs", {}).get("model"): - model_name = embedder_config["model_kwargs"]["model"] - if not check_ollama_model_exists(model_name): - raise Exception(f"Ollama model '{model_name}' not found. Please run 'ollama pull {model_name}' to install it.") - - # Initialize components - self.memory = Memory() - self.embedder = get_embedder(embedder_type=self.embedder_type) - self.initialize_db_manager() - - def initialize_db_manager(self): - """Initialize the database manager with local storage""" - self.db_manager = DatabaseManager() - self.transformed_docs = [] - - def _validate_and_filter_embeddings(self, documents: List) -> List: - """ - Validate embeddings and filter out documents with invalid or mismatched embedding sizes. - - Args: - documents: List of documents with embeddings - - Returns: - List of documents with valid embeddings of consistent size - """ - if not documents: - logger.warning("No documents provided for embedding validation") - return [] - - valid_documents = [] - embedding_sizes = {} - - # First pass: collect all embedding sizes and count occurrences - for i, doc in enumerate(documents): - if not hasattr(doc, 'vector') or doc.vector is None: - logger.warning(f"Document {i} has no embedding vector, skipping") - continue - - try: - if isinstance(doc.vector, list): - embedding_size = len(doc.vector) - elif hasattr(doc.vector, 'shape'): - embedding_size = doc.vector.shape[0] if len(doc.vector.shape) == 1 else doc.vector.shape[-1] - elif hasattr(doc.vector, '__len__'): - embedding_size = len(doc.vector) - else: - logger.warning(f"Document {i} has invalid embedding vector type: {type(doc.vector)}, skipping") - continue - - if embedding_size == 0: - logger.warning(f"Document {i} has empty embedding vector, skipping") - continue - - embedding_sizes[embedding_size] = embedding_sizes.get(embedding_size, 0) + 1 - - except Exception as e: - logger.warning(f"Error checking embedding size for document {i}: {str(e)}, skipping") - continue - - if not embedding_sizes: - logger.error("No valid embeddings found in any documents") - return [] - - # Find the most common embedding size (this should be the correct one) - target_size = max(embedding_sizes.keys(), key=lambda k: embedding_sizes[k]) - logger.info(f"Target embedding size: {target_size} (found in {embedding_sizes[target_size]} documents)") - - # Log all embedding sizes found - for size, count in embedding_sizes.items(): - if size != target_size: - logger.warning(f"Found {count} documents with incorrect embedding size {size}, will be filtered out") - - # Second pass: filter documents with the target embedding size - for i, doc in enumerate(documents): - if not hasattr(doc, 'vector') or doc.vector is None: - continue - - try: - if isinstance(doc.vector, list): - embedding_size = len(doc.vector) - elif hasattr(doc.vector, 'shape'): - embedding_size = doc.vector.shape[0] if len(doc.vector.shape) == 1 else doc.vector.shape[-1] - elif hasattr(doc.vector, '__len__'): - embedding_size = len(doc.vector) - else: - continue - - if embedding_size == target_size: - valid_documents.append(doc) - else: - # Log which document is being filtered out - file_path = getattr(doc, 'meta_data', {}).get('file_path', f'document_{i}') - logger.warning(f"Filtering out document '{file_path}' due to embedding size mismatch: {embedding_size} != {target_size}") - - except Exception as e: - file_path = getattr(doc, 'meta_data', {}).get('file_path', f'document_{i}') - logger.warning(f"Error validating embedding for document '{file_path}': {str(e)}, skipping") - continue - - logger.info(f"Embedding validation complete: {len(valid_documents)}/{len(documents)} documents have valid embeddings") - - if len(valid_documents) == 0: - logger.error("No documents with valid embeddings remain after filtering") - elif len(valid_documents) < len(documents): - filtered_count = len(documents) - len(valid_documents) - logger.warning(f"Filtered out {filtered_count} documents due to embedding issues") - - return valid_documents - - def prepare_retriever(self, repo_url_or_path: str, type: str = "github", access_token: str = None, - excluded_dirs: List[str] = None, excluded_files: List[str] = None, - included_dirs: List[str] = None, included_files: List[str] = None): - """ - Prepare the retriever for a repository. - Will load database from local storage if available. - - Args: - repo_url_or_path: URL or local path to the repository - access_token: Optional access token for private repositories - excluded_dirs: Optional list of directories to exclude from processing - excluded_files: Optional list of file patterns to exclude from processing - included_dirs: Optional list of directories to include exclusively - included_files: Optional list of file patterns to include exclusively - """ - self.initialize_db_manager() - self.repo_url_or_path = repo_url_or_path - self.transformed_docs = self.db_manager.prepare_database( - repo_url_or_path, - type, - access_token, - embedder_type=self.embedder_type, - excluded_dirs=excluded_dirs, - excluded_files=excluded_files, - included_dirs=included_dirs, - included_files=included_files - ) - logger.info(f"Loaded {len(self.transformed_docs)} documents for retrieval") - - # Validate and filter embeddings to ensure consistent sizes - self.transformed_docs = self._validate_and_filter_embeddings(self.transformed_docs) - - if not self.transformed_docs: - raise ValueError("No valid documents with embeddings found. Cannot create retriever.") - - logger.info(f"Using {len(self.transformed_docs)} documents with valid embeddings for retrieval") - - try: - # Use the appropriate embedder for retrieval - self.retriever = FAISSRetriever( - **configs["retriever"], - embedder=self.embedder, - documents=self.transformed_docs, - document_map_func=lambda doc: doc.vector, - ) - logger.info("FAISS retriever created successfully") - except Exception as e: - logger.error(f"Error creating FAISS retriever: {str(e)}") - # Try to provide more specific error information - if "All embeddings should be of the same size" in str(e): - logger.error("Embedding size validation failed. This suggests there are still inconsistent embedding sizes.") - # Log embedding sizes for debugging - sizes = [] - for i, doc in enumerate(self.transformed_docs[:10]): # Check first 10 docs - if hasattr(doc, 'vector') and doc.vector is not None: - try: - if isinstance(doc.vector, list): - size = len(doc.vector) - elif hasattr(doc.vector, 'shape'): - size = doc.vector.shape[0] if len(doc.vector.shape) == 1 else doc.vector.shape[-1] - elif hasattr(doc.vector, '__len__'): - size = len(doc.vector) - else: - size = "unknown" - sizes.append(f"doc_{i}: {size}") - except Exception: - sizes.append(f"doc_{i}: error") - logger.error(f"Sample embedding sizes: {', '.join(sizes)}") - raise - - async def aprepare_retriever( - self, - repo_url_or_path: str, - type: str = "github", - access_token: str | None = None, - excluded_dirs: list[str] | None = None, - excluded_files: list[str] | None = None, - included_dirs: list[str] | None = None, - included_files: list[str] | None = None, - ): - """Async version of the original `prepare_retriever`. - - Reuse the synchronous `prepare_retriever` implementation, but runs it in - a worker thread via `asyncio.to_thread` so that blocking operations (such - as git.clone, file io, embedding calls) do not stall the outer event loop. - Concurrency is bounded by a module-level semaphore, set by system variable - 'DEEPWIKI_MAX_CONCURRENT_RAG'. - - Args: - repo_url_or_path: URL or local path to the repository - access_token: Optional access token for private repositories - excluded_dirs: Optional list of directories to exclude from processing - excluded_files: Optional list of file patterns to exclude from processing - included_dirs: Optional list of directories to include exclusively - included_files: Optional list of file patterns to include exclusively - """ - async with _get_rag_semaphore(): - return await asyncio.to_thread( - self.prepare_retriever, - repo_url_or_path, - type=type, - access_token=access_token, - excluded_dirs=excluded_dirs, - excluded_files=excluded_files, - included_dirs=included_dirs, - included_files=included_files, - ) - - def call(self, query: str, language: str = "en") -> Tuple[List]: - """ - Process a query using RAG. - - Args: - query: The user's query - - Returns: - Tuple of (RAGAnswer, retrieved_documents) - """ - try: - retrieved_documents = self.retriever(query) - - # Fill in the documents - retrieved_documents[0].documents = [ - self.transformed_docs[doc_index] - for doc_index in retrieved_documents[0].doc_indices - ] - - return retrieved_documents - - except Exception as e: - logger.error(f"Error in RAG call: {str(e)}") - - # Create error response - error_response = RAGAnswer( - rationale="Error occurred while processing the query.", - answer=f"I apologize, but I encountered an error while processing your question. Please try again or rephrase your question." - ) - return error_response, [] - - async def acall(self, query: str, language: str = "en") -> tuple[list]: - """Async version of the original `call` method. - """ - return await asyncio.to_thread(self.call, query, language) diff --git a/api/rag/__init__.py b/api/rag/__init__.py new file mode 100644 index 000000000..a4ccfc9dd --- /dev/null +++ b/api/rag/__init__.py @@ -0,0 +1,8 @@ +from api.rag.pipeline import count_tokens, repo_index_exist +from api.rag.rag import RAG + +__all__ = [ + "RAG", + "count_tokens", + "repo_index_exist", +] diff --git a/api/rag/pipeline.py b/api/rag/pipeline.py new file mode 100644 index 000000000..ab9a34e5c --- /dev/null +++ b/api/rag/pipeline.py @@ -0,0 +1,617 @@ +import os +from pathlib import Path + +import adalflow as adal +import tiktoken +from adalflow.components.data_process import TextSplitter, ToEmbeddings +from adalflow.core.db import LocalDB +from adalflow.core.types import Document, List + +from api.config import ( + configs, + get_embedder, +) +from api.logger import get_logger +from api.repository import Repo + +logger = get_logger(__name__) + +# Maximum token limit for OpenAI embedding models +MAX_EMBEDDING_TOKENS = 8192 + + +def count_tokens( + text: str, embedder_type: str = None, is_ollama_embedder: bool = None +) -> int: + """ + Count the number of tokens in a text string using tiktoken. + + Args: + text (str): The text to count tokens for. + embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama', 'bedrock'). + If None, will be determined from configuration. + is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. + If None, will be determined from configuration. + + Returns: + int: The number of tokens in the text. + """ + try: + # Handle backward compatibility + if embedder_type is None and is_ollama_embedder is not None: + embedder_type = "ollama" if is_ollama_embedder else None + + # Determine embedder type if not specified + if embedder_type is None: + from api.config import get_embedder_type + + embedder_type = get_embedder_type() + + # Choose encoding based on embedder type + if embedder_type == "ollama": + # Ollama typically uses cl100k_base encoding + encoding = tiktoken.get_encoding("cl100k_base") + elif embedder_type == "google": + # Google uses similar tokenization to GPT models for rough estimation + encoding = tiktoken.get_encoding("cl100k_base") + elif embedder_type == "bedrock": + # Bedrock embedding models vary; use a common GPT-like encoding for rough estimation + encoding = tiktoken.get_encoding("cl100k_base") + else: # OpenAI or default + # Use OpenAI embedding model encoding + encoding = tiktoken.encoding_for_model("text-embedding-3-small") + + return len(encoding.encode(text)) + except Exception as e: + # Fallback to a simple approximation if tiktoken fails + logger.warning(f"Error counting tokens with tiktoken: {e}") + # Rough approximation: 4 characters per token + return len(text) // 4 + + +def _should_process_file( + file_path: Path, + use_inclusion: bool, + included_dirs: list[str] | None, + included_files: list[str] | None, + excluded_dirs: list[str] | None, + excluded_files: list[str] | None, +) -> bool: + """ + Determine if a file should be processed based on inclusion/exclusion rules. + + Args: + file_path (str): The file path to check + use_inclusion (bool): Whether to use inclusion mode + included_dirs (List[str]): List of directories to include + included_files (List[str]): List of files to include + excluded_dirs (List[str]): List of directories to exclude + excluded_files (List[str]): List of files to exclude + + Returns: + bool: True if the file should be processed, False otherwise + """ + if isinstance(file_path, str): + file_path = Path(file_path) + file_path_parts = file_path.resolve().parts + file_name = file_path_parts[-1] + + if use_inclusion: + # Inclusion mode: file must be in included directories or match included files + is_included = False + + # Check if file is in an included directory + if included_dirs: + for included in included_dirs: + clean_included = included.removeprefix("./").rstrip("/") + if clean_included in file_path_parts: + is_included = True + break + + # Check if file matches included file patterns + if not is_included and included_files: + for included_file in included_files: + if file_name == included_file or file_name.endswith(included_file): + is_included = True + break + + # If no inclusion rules are specified for a category, allow all files from that category + if not included_dirs and not included_files: + is_included = True + elif not included_dirs and included_files: + # Only file patterns specified, allow all directories + pass # is_included is already set based on file patterns + elif included_dirs and not included_files: + # Only directory patterns specified, allow all files in included directories + pass # is_included is already set based on directory patterns + + return is_included + else: + # Exclusion mode: file must not be in excluded directories or match excluded files + is_excluded = False + + # Check if file is in an excluded directory + if excluded_dirs: + for excluded in excluded_dirs: + clean_excluded = excluded.removeprefix("./").rstrip("/") + if clean_excluded in file_path_parts: + is_excluded = True + break + + # Check if file matches excluded file patterns + if not is_excluded and excluded_files: + for excluded_file in excluded_files: + if file_name == excluded_file: + is_excluded = True + break + + return not is_excluded + + +def read_all_documents( + path: str, + embedder_type: str = None, + is_ollama_embedder: bool = None, + excluded_dirs: list[str] | None = None, + excluded_files: list[str] | None = None, + included_dirs: list[str] | None = None, + included_files: list[str] | None = None, +): + """ + Recursively reads all documents in a directory and its subdirectories. + + Args: + path (str): The root directory path. + embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama'). + If None, will be determined from configuration. + is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. + If None, will be determined from configuration. + excluded_dirs (List[str], optional): List of directories to exclude from processing. + Overrides the default configuration if provided. + excluded_files (List[str], optional): List of file patterns to exclude from processing. + Overrides the default configuration if provided. + included_dirs (List[str], optional): List of directories to include exclusively. + When provided, only files in these directories will be processed. + included_files (List[str], optional): List of file patterns to include exclusively. + When provided, only files matching these patterns will be processed. + + Returns: + list: A list of Document objects with metadata. + """ + # Handle backward compatibility + if embedder_type is None and is_ollama_embedder is not None: + embedder_type = "ollama" if is_ollama_embedder else None + documents = [] + # File extensions to look for, prioritizing code files + code_extensions = configs.get("code_extensions", []) + doc_extensions = configs.get("doc_extensions", []) + + # Determine filtering mode: inclusion or exclusion + use_inclusion_mode = bool(included_dirs or included_files) + + if use_inclusion_mode: + # Inclusion mode: only process specified directories and files + included_dirs = list(set(included_dirs)) if included_dirs else list() + included_files = list(set(included_files)) if included_files else list() + + logger.info("Using inclusion mode") + logger.info(f"Included directories: {included_dirs}") + logger.info(f"Included files: {included_files}") + + # Convert to lists for processing + excluded_dirs = [] + excluded_files = [] + else: + # Exclusion mode: use default exclusions plus any additional ones + file_filters = configs.get("file_filters", {}) + final_excluded_dirs: set[str] = set(file_filters.get("excluded_dirs", [])) + final_excluded_files: set[str] = set(file_filters.get("excluded_files", [])) + + # Add any explicitly provided excluded directories and files + if excluded_dirs is not None: + final_excluded_dirs.update(excluded_dirs) + + if excluded_files is not None: + final_excluded_files.update(excluded_files) + + # Convert back to lists for compatibility + excluded_dirs = list(final_excluded_dirs) + excluded_files = list(final_excluded_files) + included_dirs = [] + included_files = [] + + logger.info("Using exclusion mode") + logger.info(f"Excluded directories: {excluded_dirs}") + logger.info(f"Excluded files: {excluded_files}") + + logger.info(f"Reading documents from {path}") + + for file_path in filter( + lambda p: _should_process_file( + p, + use_inclusion=use_inclusion_mode, + included_dirs=included_dirs, + included_files=included_files, + excluded_dirs=excluded_dirs, + excluded_files=excluded_files, + ), + filter( + lambda p: p.suffix.lower() in code_extensions + doc_extensions, + Path(path).rglob(pattern="**/*"), + ), + ): + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + relative_path = os.path.relpath(file_path, path) + + # Check token count + token_count = count_tokens(content, embedder_type) + if token_count > MAX_EMBEDDING_TOKENS * 10: + logger.warning( + f"Skipping large file {relative_path}: Token count ({token_count}) exceeds limit" + ) + continue + + file_ext = file_path.suffix.lower() + is_code = file_ext in code_extensions + # Determine if this is an implementation file + if is_code: + is_implementation = ( + not relative_path.startswith("test_") + and not relative_path.startswith("app_") + and "test" not in relative_path.lower() + ) + else: + is_implementation = False + + doc = Document( + text=content, + meta_data={ + "file_path": relative_path, + "type": file_ext, + "is_code": is_code, + "is_implementation": is_implementation, + "title": relative_path, + "token_count": token_count, + }, + ) + documents.append(doc) + except Exception as e: + logger.error(f"Error reading {file_path}: {e}") + + logger.info(f"Found {len(documents)} documents") + return documents + + +def get_repo_db(repo: Repo) -> str: + if not repo.root_path: + raise ValueError(f"Repo root path is empty: {repo}") + save_db_file = os.path.join(repo.root_path, "databases", f"{repo.name}.pkl") + return save_db_file + + +def repo_index_exist(repo: Repo) -> bool: + return os.path.exists(get_repo_db(repo)) + + +class LineTrackingTextSplitter(TextSplitter): + """TextSplitter that annotates each chunk with its 1-based start/end line. + + adalflow's ``TextSplitter`` deep-copies the parent ``meta_data`` once and shares + that single dict across every chunk of the same document, so we first give each + chunk its own copy before writing per-chunk line numbers. Token chunks are exact + substrings of the parent text, so each chunk is located with an order-preserving + substring search to compute its line range. + """ + + def call(self, documents): + parent_text = {doc.id: (doc.text or "") for doc in documents} + split_docs = super().call(documents) + + chunks_by_parent = {} + for chunk in split_docs: + chunks_by_parent.setdefault(chunk.parent_doc_id, []).append(chunk) + + for parent_id, chunks in chunks_by_parent.items(): + text = parent_text.get(parent_id, "") + cursor = 0 + for chunk in sorted(chunks, key=lambda c: c.order): + # Each chunk needs its own meta_data copy (the parent shares one dict). + chunk.meta_data = dict(chunk.meta_data or {}) + pos = text.find(chunk.text, cursor) + if pos == -1: + pos = text.find(chunk.text) # fall back to a global search + if pos == -1: + continue # leave line numbers unset if the chunk can't be located + start_line = text.count("\n", 0, pos) + 1 + end_line = start_line + chunk.text.count("\n") + chunk.meta_data["start_line"] = start_line + chunk.meta_data["end_line"] = end_line + cursor = pos + 1 + return split_docs + + +def prepare_data_pipeline(embedder_type: str = None, is_ollama_embedder: bool = None): + """ + Creates and returns the data transformation pipeline. + + Args: + embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama'). + If None, will be determined from configuration. + is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. + If None, will be determined from configuration. + + Returns: + adal.Sequential: The data transformation pipeline + """ + from api.config import get_embedder_config, get_embedder_type + + # Handle backward compatibility + if embedder_type is None and is_ollama_embedder is not None: + embedder_type = "ollama" if is_ollama_embedder else None + + # Determine embedder type if not specified + if embedder_type is None: + embedder_type = get_embedder_type() + + splitter = LineTrackingTextSplitter(**configs["text_splitter"]) + embedder_config = get_embedder_config() + + embedder = get_embedder(embedder_type=embedder_type) + + batch_size = embedder_config.get("batch_size", 500) + embedder_transformer = ToEmbeddings(embedder=embedder, batch_size=batch_size) + + data_transformer = adal.Sequential( + splitter, embedder_transformer + ) # sequential will chain together splitter and embedder + return data_transformer + + +def transform_documents_and_save_to_db( + documents: List[Document], + db_path: str, + embedder_type: str = None, + is_ollama_embedder: bool = None, +) -> LocalDB: + """ + Transforms a list of documents and saves them to a local database. + + Args: + documents (list): A list of `Document` objects. + db_path (str): The path to the local database file. + embedder_type (str, optional): The embedder type ('openai', 'google', 'ollama'). + If None, will be determined from configuration. + is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. + If None, will be determined from configuration. + """ + # Get the data transformer + data_transformer = prepare_data_pipeline(embedder_type, is_ollama_embedder) + + # Save the documents to a local database + db = LocalDB() + db.register_transformer(transformer=data_transformer, key="split_and_embed") + db.load(documents) + db.transform(key="split_and_embed") + os.makedirs(os.path.dirname(db_path), exist_ok=True) + db.save_state(filepath=db_path) + return db + + +class DatabaseManager: + """ + Manages the creation, loading, transformation, and persistence of LocalDB instances. + """ + + def __init__(self): + self.db = None + self.repo_url_or_path = None + self.repo_paths = None + + def prepare_database( + self, + repo_url_or_path: str, + repo_type: str = None, + access_token: str = None, + embedder_type: str = None, + is_ollama_embedder: bool = None, + excluded_dirs: List[str] = None, + excluded_files: List[str] = None, + included_dirs: List[str] = None, + included_files: List[str] = None, + ) -> List[Document]: + """ + Create a new database from the repository. + + Args: + repo_type(str): Type of repository + repo_url_or_path (str): The URL or local path of the repository + access_token (str, optional): Access token for private repositories + embedder_type (str, optional): Embedder type to use ('openai', 'google', 'ollama'). + If None, will be determined from configuration. + is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. + If None, will be determined from configuration. + excluded_dirs (List[str], optional): List of directories to exclude from processing + excluded_files (List[str], optional): List of file patterns to exclude from processing + included_dirs (List[str], optional): List of directories to include exclusively + included_files (List[str], optional): List of file patterns to include exclusively + + Returns: + List[Document]: List of Document objects + """ + # Handle backward compatibility + if embedder_type is None and is_ollama_embedder is not None: + embedder_type = "ollama" if is_ollama_embedder else None + + self.reset_database() + self._create_repo(repo_url_or_path, repo_type, access_token) + return self.prepare_db_index( + embedder_type=embedder_type, + excluded_dirs=excluded_dirs, + excluded_files=excluded_files, + included_dirs=included_dirs, + included_files=included_files, + ) + + def reset_database(self): + """ + Reset the database to its initial state. + """ + self.db = None + self.repo_url_or_path = None + self.repo_paths = None + + def _create_repo( + self, repo_url_or_path: str, repo_type: str = None, access_token: str = None + ) -> None: + """ + Download and prepare all paths. + Paths: + ~/.adalflow/repos/{owner}_{repo_name} (for url, local path will be the same) + ~/.adalflow/databases/{owner}_{repo_name}.pkl + + Args: + repo_type(str): Type of repository + repo_url_or_path (str): The URL or local path of the repository + access_token (str, optional): Access token for private repositories + """ + logger.info(f"Preparing repo storage for {repo_url_or_path}...") + + try: + # Strip whitespace to handle URLs with leading/trailing spaces + repo_url_or_path = repo_url_or_path.strip() + repo = Repo( + repo_url=repo_url_or_path, + repo_type=repo_type, + access_token=access_token, + ) + logger.info(f"Extracted repo name: {repo.name}") + if not repo.downloaded: + repo.download() + else: + logger.info( + f"Repository already exists at {repo.save_path}. Using existing repository." + ) + + save_db_file = get_repo_db(repo) + os.makedirs(os.path.dirname(save_db_file), exist_ok=True) + + self.repo_paths = { + "save_repo_dir": repo.save_path, + "save_db_file": save_db_file, + } + self.repo_url_or_path = repo_url_or_path + logger.info(f"Repo paths: {self.repo_paths}") + + except Exception as e: + logger.error(f"Failed to create repository structure: {e}") + raise + + def prepare_db_index( + self, + embedder_type: str = None, + is_ollama_embedder: bool = None, + excluded_dirs: List[str] = None, + excluded_files: List[str] = None, + included_dirs: List[str] = None, + included_files: List[str] = None, + ) -> List[Document]: + """ + Prepare the indexed database for the repository. + + Args: + embedder_type (str, optional): Embedder type to use ('openai', 'google', 'ollama'). + If None, will be determined from configuration. + is_ollama_embedder (bool, optional): DEPRECATED. Use embedder_type instead. + If None, will be determined from configuration. + excluded_dirs (List[str], optional): List of directories to exclude from processing + excluded_files (List[str], optional): List of file patterns to exclude from processing + included_dirs (List[str], optional): List of directories to include exclusively + included_files (List[str], optional): List of file patterns to include exclusively + + Returns: + List[Document]: List of Document objects + """ + + def _embedding_vector_length(doc: Document) -> int: + vector = getattr(doc, "vector", None) + if vector is None: + return 0 + try: + if hasattr(vector, "shape"): + if len(vector.shape) == 0: + return 0 + return int(vector.shape[-1]) + if hasattr(vector, "__len__"): + return int(len(vector)) + except Exception: + return 0 + return 0 + + # Handle backward compatibility + if embedder_type is None and is_ollama_embedder is not None: + embedder_type = "ollama" if is_ollama_embedder else None + # check the database + if self.repo_paths and os.path.exists(self.repo_paths["save_db_file"]): + logger.info("Loading existing database...") + try: + self.db = LocalDB.load_state(self.repo_paths["save_db_file"]) + documents = self.db.get_transformed_data(key="split_and_embed") + if documents: + lengths = [_embedding_vector_length(doc) for doc in documents] + non_empty = sum(1 for n in lengths if n > 0) + empty = len(lengths) - non_empty + sample_sizes = sorted({n for n in lengths if n > 0})[:3] + logger.info( + "Loaded %s documents from existing database (embeddings: %s non-empty, %s empty; sample_dims=%s)", + len(documents), + non_empty, + empty, + sample_sizes, + ) + + if non_empty == 0: + logger.warning( + "Existing database contains no usable embeddings. Rebuilding embeddings..." + ) + else: + return documents + except Exception as e: + logger.error(f"Error loading existing database: {e}") + # Continue to create a new database + + # prepare the database + logger.info("Creating new database...") + documents = read_all_documents( + self.repo_paths["save_repo_dir"], + embedder_type=embedder_type, + excluded_dirs=excluded_dirs, + excluded_files=excluded_files, + included_dirs=included_dirs, + included_files=included_files, + ) + self.db = transform_documents_and_save_to_db( + documents, self.repo_paths["save_db_file"], embedder_type=embedder_type + ) + logger.info(f"Total documents: {len(documents)}") + transformed_docs = self.db.get_transformed_data(key="split_and_embed") + logger.info(f"Total transformed documents: {len(transformed_docs)}") + return transformed_docs + + def prepare_retriever( + self, repo_url_or_path: str, repo_type: str = None, access_token: str = None + ): + """ + Prepare the retriever for a repository. + This is a compatibility method for the isolated API. + + Args: + repo_type(str): Type of repository + repo_url_or_path (str): The URL or local path of the repository + access_token (str, optional): Access token for private repositories + + Returns: + List[Document]: List of Document objects + """ + return self.prepare_database(repo_url_or_path, repo_type, access_token) diff --git a/api/rag/rag.py b/api/rag/rag.py new file mode 100644 index 000000000..c9aa57d5a --- /dev/null +++ b/api/rag/rag.py @@ -0,0 +1,385 @@ +import asyncio +import os +from collections import defaultdict +from collections.abc import Sized +from uuid import uuid4 + +import adalflow as adal +from adalflow.components.retriever.faiss_retriever import FAISSRetriever +from adalflow.core.types import ( + AssistantResponse, + DialogTurn, + Document, + RetrieverOutput, + UserQuery, +) + +from api.config import configs, get_embedder +from api.logger import get_logger +from api.rag.pipeline import DatabaseManager + +logger = get_logger(__name__) + +# Maximum concurrent RAG preparing count +_RAG_PREPARE_SEMAPHORE: asyncio.Semaphore | None = None + + +def _get_rag_semaphore() -> asyncio.Semaphore: + global _RAG_PREPARE_SEMAPHORE + if _RAG_PREPARE_SEMAPHORE is None: + _RAG_PREPARE_SEMAPHORE = asyncio.Semaphore( + int(os.environ.get("DEEPWIKI_MAX_CONCURRENT_RAG", "4")) + ) + assert isinstance(_RAG_PREPARE_SEMAPHORE, asyncio.Semaphore) + return _RAG_PREPARE_SEMAPHORE + + +def check_ollama_model_exists(model_name: str, ollama_host: str | None = None) -> bool: + """ + Check if an Ollama model exists before attempting to use it. + + Args: + model_name: Name of the model to check + ollama_host: Ollama host URL, defaults to localhost:11434 + + Returns: + bool: True if model exists, False otherwise + """ + import httpx + import ollama + + if ollama_host is None: + ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434") + try: + # Remove /api prefix if present and add it back + ollama_host = ollama_host.removesuffix("/api") + ret: ollama.ListResponse = ollama.Client(host=ollama_host, timeout=5).list() + is_available = any(model_name == model.model for model in ret.models) + if is_available: + logger.info("Ollama model '%s' is available", model_name) + else: + logger.warning( + "Ollama model '%s' is not available. Available models: %s. ", + model_name, + str([model.model for model in ret.models]), + ) + return is_available + except (httpx.ConnectTimeout, ConnectionError) as e: + logger.warning(f"Could not connect to Ollama to check models: {e}") + return False + except Exception as e: + logger.warning(f"Error checking Ollama model availability: {e}") + return False + + +class CustomConversation(list[DialogTurn]): + """Custom implementation of Conversation to fix the list assignment index out of range error""" + + +class Memory(adal.core.component.DataComponent): + """Simple conversation management with a list of dialog turns.""" + + def __init__(self): + super().__init__() + # Use our custom implementation instead of the original Conversation class + self.current_conversation = CustomConversation() + + def call(self) -> dict: + """Return the conversation history as a dictionary.""" + all_dialog_turns = { + dialog_turn.id: dialog_turn for dialog_turn in self.current_conversation + } + logger.info(f"Returning {len(all_dialog_turns)} dialog turns from memory") + return all_dialog_turns + + def add_dialog_turn(self, user_query: str, assistant_response: str) -> None: + """ + Add a dialog turn to the conversation history. + + Args: + user_query: The user's query + assistant_response: The assistant's response + + """ + # Create a new dialog turn using our custom implementation + dialog_turn = DialogTurn( + id=str(uuid4()), + user_query=UserQuery(query_str=user_query), + assistant_response=AssistantResponse(response_str=assistant_response), + ) + + # Safely append the dialog turn + self.current_conversation.append(dialog_turn) + logger.info( + "Successfully added dialog turn, now have %d turns", + len(self.current_conversation), + ) + + +def _get_document_vector_size(document: Document) -> int | None: + if hasattr(document.vector, "shape"): + embedding_size = ( + document.vector.shape[0] + if len(document.vector.shape) == 1 + else document.vector.shape[-1] + ) + elif isinstance(document.vector, Sized): + embedding_size = len(document.vector) + else: + embedding_size = None + + return embedding_size + + +class RAG(adal.Component): + """RAG with one repo. + If you want to load a new repos, call prepare_retriever(repo_url_or_path) first.""" + + def __init__(self, provider="google", model=None, use_s3: bool = False): # noqa: F841 - use_s3 is kept for compatibility + """ + Initialize the RAG component. + + Args: + provider: Model provider to use (google, openai, openrouter, ollama) + model: Model name to use with the provider + use_s3: Whether to use S3 for database storage (default: False) + """ + super().__init__() + + self.provider = provider + self.model = model + + # Import the helper functions + from api.config import get_embedder_type + + # Determine embedder type based on current configuration + self.embedder_type = get_embedder_type() + self.is_ollama_embedder = ( + self.embedder_type == "ollama" + ) # Backward compatibility + + # Check if Ollama model exists before proceeding + if self.is_ollama_embedder: + from api.config import get_embedder_config + + embedder_config = get_embedder_config() + if embedder_config and embedder_config.get("model_kwargs", {}).get("model"): + model_name = embedder_config["model_kwargs"]["model"] + if not check_ollama_model_exists(model_name): + raise ValueError( + f"Ollama model '{model_name}' not found. Please run 'ollama pull {model_name}' to install it." + ) + + # Initialize components + self.memory = Memory() + self.embedder = get_embedder(embedder_type=self.embedder_type) + self.initialize_db_manager() + + def initialize_db_manager(self): + """Initialize the database manager with local storage""" + self.db_manager = DatabaseManager() + self.transformed_docs = [] + + @staticmethod + def _validate_and_filter_embeddings(documents: list[Document]) -> list: + """ + Validate embeddings and filter out documents with invalid or mismatched embedding sizes. + + Args: + documents: List of documents with embeddings + + Returns: + List of documents with valid embeddings of consistent size + """ + if not documents: + logger.warning("No documents provided for embedding validation") + return [] + + docs_embeddings = defaultdict(list) + + for doc, embed_size in filter( + lambda x: isinstance(x[0], Document) and bool(x[1]), + ((x, _get_document_vector_size(x)) for x in documents), + ): + docs_embeddings[embed_size].append(doc) + if not docs_embeddings: + logger.error("No valid embeddings found in any documents") + return [] + + target_size = max(docs_embeddings, key=lambda x: len(docs_embeddings[x])) + logger.info( + "Target embedding size: %s (found in %s documents)", + target_size, + len(docs_embeddings[target_size]), + ) + + valid_documents = docs_embeddings.pop(target_size) + + if docs_embeddings: + for embed_size, docs_list in docs_embeddings.items(): + logger.warning( + "Found %s documents with incorrect embedding size %s, will be filtered out.", + len(docs_list), + str(embed_size), + ) + + if not valid_documents: + logger.warning( + "No documents with valid embeddings remained after filtering" + ) + else: + logger.info( + "Embedding validation complete: %d/%d documents have valid embeddings.", + len(valid_documents), + len(documents), + ) + return valid_documents + + def prepare_retriever( + self, + repo_url_or_path: str, + type: str = "github", + access_token: str | None = None, + excluded_dirs: list[str] | None = None, + excluded_files: list[str] | None = None, + included_dirs: list[str] | None = None, + included_files: list[str] | None = None, + ): + """ + Prepare the retriever for a repository. + Will load database from local storage if available. + + Args: + repo_url_or_path: URL or local path to the repository + access_token: Optional access token for private repositories + excluded_dirs: Optional list of directories to exclude from processing + excluded_files: Optional list of file patterns to exclude from processing + included_dirs: Optional list of directories to include exclusively + included_files: Optional list of file patterns to include exclusively + """ + self.initialize_db_manager() + self.repo_url_or_path = repo_url_or_path + self.transformed_docs = self.db_manager.prepare_database( + repo_url_or_path, + type, + access_token, + embedder_type=self.embedder_type, + excluded_dirs=excluded_dirs, + excluded_files=excluded_files, + included_dirs=included_dirs, + included_files=included_files, + ) + logger.info(f"Loaded {len(self.transformed_docs)} documents for retrieval") + + # Validate and filter embeddings to ensure consistent sizes + self.transformed_docs = self._validate_and_filter_embeddings( + self.transformed_docs + ) + + if not self.transformed_docs: + raise ValueError( + "No valid documents with embeddings found. Cannot create retriever." + ) + + logger.info( + f"Using {len(self.transformed_docs)} documents with valid embeddings for retrieval" + ) + + try: + # Use the appropriate embedder for retrieval + self.retriever = FAISSRetriever( + **configs["retriever"], + embedder=self.embedder, + documents=self.transformed_docs, + document_map_func=lambda doc: doc.vector, + ) + logger.info("FAISS retriever created successfully") + except Exception as e: + logger.error(f"Error creating FAISS retriever: {str(e)}") + # Try to provide more specific error information + if "All embeddings should be of the same size" in str(e): + logger.error( + "Embedding size validation failed. This suggests there are still inconsistent embedding sizes." + ) + # Log embedding sizes for debugging + sizes = [] + for i, doc in enumerate( + self.transformed_docs[:10] + ): # Check first 10 docs + if hasattr(doc, "vector") and doc.vector is not None: + try: + size = _get_document_vector_size(doc) or "unknown" + sizes.append(f"doc_{i}: {size}") + except Exception: + sizes.append(f"doc_{i}: error") + logger.error(f"Sample embedding sizes: {', '.join(sizes)}") + raise + + async def aprepare_retriever( + self, + repo_url_or_path: str, + type: str = "github", + access_token: str | None = None, + excluded_dirs: list[str] | None = None, + excluded_files: list[str] | None = None, + included_dirs: list[str] | None = None, + included_files: list[str] | None = None, + ): + """Async version of the original `prepare_retriever`. + + Reuse the synchronous `prepare_retriever` implementation, but runs it in + a worker thread via `asyncio.to_thread` so that blocking operations (such + as git.clone, file io, embedding calls) do not stall the outer event loop. + Concurrency is bounded by a module-level semaphore, set by system variable + 'DEEPWIKI_MAX_CONCURRENT_RAG'. + + Args: + repo_url_or_path: URL or local path to the repository + access_token: Optional access token for private repositories + excluded_dirs: Optional list of directories to exclude from processing + excluded_files: Optional list of file patterns to exclude from processing + included_dirs: Optional list of directories to include exclusively + included_files: Optional list of file patterns to include exclusively + """ + async with _get_rag_semaphore(): + return await asyncio.to_thread( + self.prepare_retriever, + repo_url_or_path, + type=type, + access_token=access_token, + excluded_dirs=excluded_dirs, + excluded_files=excluded_files, + included_dirs=included_dirs, + included_files=included_files, + ) + + def call( + self, query: str | list[str], language: str = "en" + ) -> list[RetrieverOutput]: + """ + Process a query using RAG. + + Args: + query: The user's query + + Returns: + list of RetrieverOutput. + """ + try: + retrieved_documents = self.retriever(query) + + # Fill in the documents + retrieved_documents[0].documents = [ + self.transformed_docs[doc_index] + for doc_index in retrieved_documents[0].doc_indices + ] + + return retrieved_documents + + except Exception: + logger.exception("Error in RAG call, returning empty list") + return [] + + async def acall(self, query: str, language: str = "en") -> list[RetrieverOutput]: + """Async version of the original `call` method.""" + return await asyncio.to_thread(self.call, query, language) diff --git a/api/repository.py b/api/repository.py new file mode 100644 index 000000000..e56c633b8 --- /dev/null +++ b/api/repository.py @@ -0,0 +1,516 @@ +import base64 +import json +import os +import subprocess +from urllib.parse import quote, urlparse, urlunparse + +import requests +from adalflow.utils import get_adalflow_default_root_path +from requests.exceptions import RequestException + +from api.logger import get_logger + +logger = get_logger(__name__) + + +CLONE_REPO_ROOT = os.path.join(get_adalflow_default_root_path(), "repo") + + +def _get_github_file_content( + repo_url: str, file_path: str, access_token: str = None +) -> str: + """ + Retrieves the content of a file from a GitHub repository using the GitHub API. + Supports both public GitHub (github.com) and GitHub Enterprise (custom domains). + + Args: + repo_url (str): The URL of the GitHub repository + (e.g., "https://github.com/username/repo" or "https://github.company.com/username/repo") + file_path (str): The path to the file within the repository (e.g., "src/main.py") + access_token (str, optional): GitHub personal access token for private repositories + + Returns: + str: The content of the file as a string + + Raises: + ValueError: If the file cannot be fetched or if the URL is not a valid GitHub URL + """ + try: + # Parse the repository URL to support both github.com and enterprise GitHub + parsed_url = urlparse(repo_url) + if not parsed_url.scheme or not parsed_url.netloc: + raise ValueError("Not a valid GitHub repository URL") + + # Check if it's a GitHub-like URL structure + path_parts = parsed_url.path.strip("/").split("/") + if len(path_parts) < 2: + raise ValueError( + "Invalid GitHub URL format - expected format: https://domain/owner/repo" + ) + + owner = path_parts[-2] + repo = path_parts[-1].replace(".git", "") + + # Determine the API base URL + if parsed_url.netloc == "github.com": + # Public GitHub + api_base = "https://api.github.com" + else: + # GitHub Enterprise - API is typically at https://domain/api/v3/ + api_base = f"{parsed_url.scheme}://{parsed_url.netloc}/api/v3" + + # Use GitHub API to get file content + # The API endpoint for getting file content is: /repos/{owner}/{repo}/contents/{path} + api_url = f"{api_base}/repos/{owner}/{repo}/contents/{file_path}" + + # Fetch file content from GitHub API + headers = {} + if access_token: + headers["Authorization"] = f"token {access_token}" + logger.info(f"Fetching file content from GitHub API: {api_url}") + try: + response = requests.get(api_url, headers=headers) + response.raise_for_status() + except RequestException as e: + raise ValueError(f"Error fetching file content: {e}") + try: + content_data = response.json() + except json.JSONDecodeError: + raise ValueError("Invalid response from GitHub API") + + # Check if we got an error response + if "message" in content_data and "documentation_url" in content_data: + raise ValueError(f"GitHub API error: {content_data['message']}") + + # GitHub API returns file content as base64 encoded string + if "content" in content_data and "encoding" in content_data: + if content_data["encoding"] == "base64": + # The content might be split into lines, so join them first + content_base64 = content_data["content"].replace("\n", "") + content = base64.b64decode(content_base64).decode("utf-8") + return content + else: + raise ValueError(f"Unexpected encoding: {content_data['encoding']}") + else: + raise ValueError("File content not found in GitHub API response") + + except Exception as e: + raise ValueError(f"Failed to get file content: {str(e)}") + + +def _get_gitlab_file_content( + repo_url: str, file_path: str, access_token: str = None +) -> str: + """ + Retrieves the content of a file from a GitLab repository (cloud or self-hosted). + + Args: + repo_url (str): The GitLab repo URL (e.g., "https://gitlab.com/username/repo" or "http://localhost/group/project") + file_path (str): File path within the repository (e.g., "src/main.py") + access_token (str, optional): GitLab personal access token + + Returns: + str: File content + + Raises: + ValueError: If anything fails + """ + try: + # Parse and validate the URL + parsed_url = urlparse(repo_url) + if not parsed_url.scheme or not parsed_url.netloc: + raise ValueError("Not a valid GitLab repository URL") + + gitlab_domain = f"{parsed_url.scheme}://{parsed_url.netloc}" + if parsed_url.port not in (None, 80, 443): + gitlab_domain += f":{parsed_url.port}" + path_parts = parsed_url.path.strip("/").split("/") + if len(path_parts) < 2: + raise ValueError( + "Invalid GitLab URL format — expected something like https://gitlab.domain.com/group/project" + ) + + # Build project path and encode for API + project_path = "/".join(path_parts).replace(".git", "") + encoded_project_path = quote(project_path, safe="") + + # Encode file path + encoded_file_path = quote(file_path, safe="") + + # Try to get the default branch from the project info + default_branch = None + try: + project_info_url = f"{gitlab_domain}/api/v4/projects/{encoded_project_path}" + project_headers = {} + if access_token: + project_headers["PRIVATE-TOKEN"] = access_token + + project_response = requests.get(project_info_url, headers=project_headers) + if project_response.status_code == 200: + project_data = project_response.json() + default_branch = project_data.get("default_branch", "main") + logger.info(f"Found default branch: {default_branch}") + else: + logger.warning( + "Could not fetch project info, using 'main' as default branch" + ) + default_branch = "main" + except Exception as e: + logger.warning( + f"Error fetching project info: {e}, using 'main' as default branch" + ) + default_branch = "main" + + api_url = f"{gitlab_domain}/api/v4/projects/{encoded_project_path}/repository/files/{encoded_file_path}/raw?ref={default_branch}" + # Fetch file content from GitLab API + headers = {} + if access_token: + headers["PRIVATE-TOKEN"] = access_token + logger.info(f"Fetching file content from GitLab API: {api_url}") + try: + response = requests.get(api_url, headers=headers) + response.raise_for_status() + content = response.text + except RequestException as e: + raise ValueError(f"Error fetching file content: {e}") + + # Check for GitLab error response (JSON instead of raw file) + if content.startswith("{") and '"message":' in content: + try: + error_data = json.loads(content) + if "message" in error_data: + raise ValueError(f"GitLab API error: {error_data['message']}") + except json.JSONDecodeError: + pass + + return content + + except Exception as e: + raise ValueError(f"Failed to get file content: {str(e)}") + + +def _get_bitbucket_file_content( + repo_url: str, file_path: str, access_token: str = None +) -> str: + """ + Retrieves the content of a file from a Bitbucket repository using the Bitbucket API. + + Args: + repo_url (str): The URL of the Bitbucket repository (e.g., "https://bitbucket.org/username/repo") + file_path (str): The path to the file within the repository (e.g., "src/main.py") + access_token (str, optional): Bitbucket personal access token for private repositories + + Returns: + str: The content of the file as a string + """ + try: + # Extract owner and repo name from Bitbucket URL + if not ( + repo_url.startswith("https://bitbucket.org/") + or repo_url.startswith("http://bitbucket.org/") + ): + raise ValueError("Not a valid Bitbucket repository URL") + + parts = repo_url.rstrip("/").split("/") + if len(parts) < 5: + raise ValueError("Invalid Bitbucket URL format") + + owner = parts[-2] + repo = parts[-1].replace(".git", "") + + # Try to get the default branch from the repository info + default_branch = None + try: + repo_info_url = f"https://api.bitbucket.org/2.0/repositories/{owner}/{repo}" + repo_headers = {} + if access_token: + repo_headers["Authorization"] = f"Bearer {access_token}" + + repo_response = requests.get(repo_info_url, headers=repo_headers) + if repo_response.status_code == 200: + repo_data = repo_response.json() + default_branch = repo_data.get("mainbranch", {}).get("name", "main") + logger.info(f"Found default branch: {default_branch}") + else: + logger.warning( + "Could not fetch repository info, using 'main' as default branch" + ) + default_branch = "main" + except Exception as e: + logger.warning( + f"Error fetching repository info: {e}, using 'main' as default branch" + ) + default_branch = "main" + + # Use Bitbucket API to get file content + # The API endpoint for getting file content is: /2.0/repositories/{owner}/{repo}/src/{branch}/{path} + api_url = f"https://api.bitbucket.org/2.0/repositories/{owner}/{repo}/src/{default_branch}/{file_path}" + + # Fetch file content from Bitbucket API + headers = {} + if access_token: + headers["Authorization"] = f"Bearer {access_token}" + logger.info(f"Fetching file content from Bitbucket API: {api_url}") + try: + response = requests.get(api_url, headers=headers) + if response.status_code == 200: + content = response.text + elif response.status_code == 404: + raise ValueError( + "File not found on Bitbucket. Please check the file path and repository." + ) + elif response.status_code == 401: + raise ValueError( + "Unauthorized access to Bitbucket. Please check your access token." + ) + elif response.status_code == 403: + raise ValueError( + "Forbidden access to Bitbucket. You might not have permission to access this file." + ) + elif response.status_code == 500: + raise ValueError( + "Internal server error on Bitbucket. Please try again later." + ) + else: + response.raise_for_status() + content = response.text + return content + except RequestException as e: + raise ValueError(f"Error fetching file content: {e}") + + except Exception as e: + raise ValueError(f"Failed to get file content: {str(e)}") + + +def get_repo_content( + repo_url: str, file_path: str, repo_type: str = None, access_token: str = None +) -> str: + """ + Retrieves the content of a file from a Git repository (GitHub or GitLab). + + Args: + repo_type (str): Type of repository + repo_url (str): The URL of the repository + file_path (str): The path to the file within the repository + access_token (str, optional): Access token for private repositories + + Returns: + str: The content of the file as a string + + Raises: + ValueError: If the file cannot be fetched or if the URL is not valid + """ + if repo_type == "github": + return _get_github_file_content(repo_url, file_path, access_token) + elif repo_type == "gitlab": + return _get_gitlab_file_content(repo_url, file_path, access_token) + elif repo_type == "bitbucket": + return _get_bitbucket_file_content(repo_url, file_path, access_token) + else: + raise ValueError( + "Unsupported repository type. Only GitHub, GitLab, and Bitbucket are supported." + ) + + +def download_repo( + repo_url: str, local_path: str, repo_type: str = None, access_token: str = None +) -> str: + """ + Downloads a Git repository (GitHub, GitLab, or Bitbucket) to a specified local path. + + Args: + repo_type(str): Type of repository + repo_url (str): The URL of the Git repository to clone. + local_path (str): The local directory where the repository will be cloned. + access_token (str, optional): Access token for private repositories. + + Returns: + str: The output message from the `git` command. + """ + try: + # Check if Git is installed + logger.info(f"Preparing to clone repository to {local_path}") + subprocess.run( + ["git", "--version"], + check=True, + capture_output=True, + ) + + # Check if repository already exists + if os.path.exists(local_path) and os.listdir(local_path): + # Directory exists and is not empty + logger.warning( + f"Repository already exists at {local_path}. Using existing repository." + ) + return f"Using existing repository at {local_path}" + + # Ensure the local path exists + os.makedirs(local_path, exist_ok=True) + + # Prepare the clone URL with access token if provided + clone_url = repo_url + if access_token: + parsed = urlparse(repo_url) + # URL-encode the token to handle special characters + encoded_token = quote(access_token, safe="") + # Determine the repository type and format the URL accordingly + if repo_type == "github": + # Format: https://{token}@{domain}/owner/repo.git + # Works for both github.com and enterprise GitHub domains + clone_url = urlunparse( + ( + parsed.scheme, + f"{encoded_token}@{parsed.netloc}", + parsed.path, + "", + "", + "", + ) + ) + elif repo_type == "gitlab": + # Format: https://oauth2:{token}@gitlab.com/owner/repo.git + clone_url = urlunparse( + ( + parsed.scheme, + f"oauth2:{encoded_token}@{parsed.netloc}", + parsed.path, + "", + "", + "", + ) + ) + elif repo_type == "bitbucket": + # Bitbucket has two token formats with different auth schemes: + # - HTTP access tokens (prefix "ATCTT") use x-bitbucket-api-token-auth + # - App passwords (deprecated, EOL June 2026) use x-token-auth + # Detect by token prefix so existing app password users keep working. + if access_token.startswith("ATCTT"): + auth_scheme = "x-bitbucket-api-token-auth" + else: + auth_scheme = "x-token-auth" + # Format: https://{auth_scheme}:{token}@bitbucket.org/owner/repo.git + clone_url = urlunparse( + ( + parsed.scheme, + f"{auth_scheme}:{encoded_token}@{parsed.netloc}", + parsed.path, + "", + "", + "", + ) + ) + + logger.info("Using access token for authentication") + + # Clone the repository + logger.info(f"Cloning repository from {repo_url} to {local_path}") + # We use repo_url in the log to avoid exposing the token in logs + result = subprocess.run( + ["git", "clone", "--depth=1", "--single-branch", clone_url, local_path], + check=True, + capture_output=True, + ) + + logger.info("Repository cloned successfully") + return result.stdout.decode("utf-8") + + except subprocess.CalledProcessError as e: + error_msg = e.stderr.decode("utf-8") + # Sanitize error message to remove any tokens (both raw and URL-encoded) + if access_token: + # Remove raw token + error_msg = error_msg.replace(access_token, "***TOKEN***") + # Also remove URL-encoded token to prevent leaking encoded version + encoded_token = quote(access_token, safe="") + error_msg = error_msg.replace(encoded_token, "***TOKEN***") + raise ValueError(f"Error during cloning: {error_msg}") + except Exception as e: + raise ValueError(f"An unexpected error occurred: {str(e)}") + + +def _path_is_url(path: str) -> bool: + """Check if the given path is a URL, or local path string. + + Parameters + ---------- + path: str + The path to be checked + + Returns + ------- + bool. True if is a URL, False otherwise + """ + try: + result = urlparse(path) + return result.scheme in {"http", "https", "ftp"} and bool(result.netloc) + except Exception: + return False + + +class Repo: + def __init__( + self, + repo_url: str, + repo_type: str | None, + root_path: str = CLONE_REPO_ROOT, + access_token: str | None = None, + ): + """ + + Parameters + ---------- + repo_url + repo_type + root_path + access_token : str, optional + The access token to use when cloning repository from a private git service. + """ + self.repo_url = repo_url + self.repo_type = repo_type + + os.makedirs(root_path, exist_ok=True) + self.root_path = root_path + self.access_token = access_token + + @property + def name(self): + return self._extract_repo_name(self.repo_url, repo_type=self.repo_type) + + @property + def is_local(self) -> bool: + return not _path_is_url(self.repo_url) + + @staticmethod + def _extract_repo_name(repo_url: str, repo_type: str | None) -> str: + if _path_is_url(repo_url): + url_parts = repo_url.rstrip("/").split("/") + if repo_type in ["github", "gitlab", "bitbucket"] and len(url_parts) >= 5: + # GitHub URL format: https://github.com/owner/repo + # GitLab URL format: https://gitlab.com/owner/repo or https://gitlab.com/group/subgroup/repo + # Bitbucket URL format: https://bitbucket.org/owner/repo + owner = url_parts[-2] + repo = url_parts[-1].replace(".git", "") + repo_name = f"{owner}_{repo}" + else: + repo_name = url_parts[-1].replace(".git", "") + else: + # This is a local repository + repo_name = os.path.basename(repo_url) + return repo_name + + def download(self, force: bool = False) -> None: + if force or (not self.downloaded and not self.is_local): + os.makedirs(self.save_path, exist_ok=True) + download_repo( + self.repo_url, self.save_path, self.repo_type, self.access_token + ) + + @property + def save_path(self) -> str: + if self.is_local: + return self.repo_url + return os.path.join(self.root_path, self.name) + + @property + def downloaded(self) -> bool: + return os.path.exists(self.save_path) and bool(os.listdir(self.save_path)) diff --git a/api/routers/__init__.py b/api/routers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/routers/auth.py b/api/routers/auth.py new file mode 100644 index 000000000..9607e2317 --- /dev/null +++ b/api/routers/auth.py @@ -0,0 +1,22 @@ +from fastapi import APIRouter + +from api.config import WIKI_AUTH_CODE, WIKI_AUTH_MODE +from api.schemas import AuthorizationConfig + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.get("/status") +async def get_auth_status(): + """ + Check if authentication is required for the wiki. + """ + return {"auth_required": WIKI_AUTH_MODE} + + +@router.post("/validate") +async def validate_auth_code(request: AuthorizationConfig): + """ + Check authorization code. + """ + return {"success": WIKI_AUTH_CODE == request.code} diff --git a/api/routers/chat.py b/api/routers/chat.py new file mode 100644 index 000000000..b33a92529 --- /dev/null +++ b/api/routers/chat.py @@ -0,0 +1,116 @@ +from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect +from fastapi.responses import StreamingResponse +from fastapi.websockets import WebSocketState + +from api.logger import get_logger +from api.schemas import ChatCompletionRequest +from api.services.research import RepoNotIndexedError, research_chat + +logger = get_logger(__name__) + + +router = APIRouter(tags=["chat"]) + + +async def _send_if_connect(websocket: WebSocket, msg: str): + if websocket.application_state == WebSocketState.CONNECTED: + await websocket.send_text(msg) + + +@router.websocket("/ws/chat") +async def handle_websocket_chat(websocket: WebSocket): + """ + Handle WebSocket connection for chat completions. + This replaces the HTTP streaming endpoint with a WebSocket connection. + """ + await websocket.accept() + try: + request = ChatCompletionRequest(**await websocket.receive_json()) + if not request.messages or len(request.messages) == 0: + await websocket.send_text("Error: No messages provided") + return + + last_message = request.messages[-1] + if last_message.role != "user": + await websocket.send_text("Error: Last message must be from the user") + return + + async for chunk in await research_chat(request): + await websocket.send_text(chunk) + + except WebSocketDisconnect: + logger.info("WebSocket disconnected") + except RepoNotIndexedError as e: + await _send_if_connect(websocket, str(e)) + except ValueError as e: + if "No valid documents with embeddings found" in str(e): + txt_message = "Error: No valid document embeddings found. This may be due to embedding size inconsistencies or API errors during document processing. Please try again or check your repository content." + else: + txt_message = f"Error preparing retriever: {str(e)}" + try: + await _send_if_connect(websocket, txt_message) + except Exception: + pass + except Exception as e: + # Check for specific embedding-related errors + if "All embeddings should be of the same size" in str(e): + txt_message = "Error: Inconsistent embedding sizes detected. Some documents may have failed to embed properly. Please try again." + else: + txt_message = f"Error preparing retriever: {str(e)}" + try: + await _send_if_connect(websocket, txt_message) + except Exception: + pass + finally: + if websocket.application_state == WebSocketState.CONNECTED: + await websocket.close() + + +@router.post("/chat/completions/stream") +async def chat_completions_stream(request: ChatCompletionRequest): + """Stream a chat completion response directly using Google Generative AI""" # Validate request + if not request.messages or len(request.messages) == 0: + raise HTTPException(status_code=400, detail="No messages provided") + + last_message = request.messages[-1] + if last_message.role != "user": + raise HTTPException( + status_code=400, detail="Last message must be from the user" + ) + + try: + async_respond = await research_chat(request=request) + + except RepoNotIndexedError as e: + raise HTTPException(status_code=425, detail=str(e)) + except ValueError as e: + if "No valid documents with embeddings found" in str(e): + raise HTTPException( + status_code=500, + detail="No valid document embeddings found. This may be due to embedding size inconsistencies or API errors during document processing. Please try again or check your repository content.", + ) + else: + raise HTTPException( + status_code=500, detail=f"Error preparing retriever: {str(e)}" + ) + except Exception as e: + if "All embeddings should be of the same size" in str(e): + raise HTTPException( + status_code=500, + detail="Inconsistent embedding sizes detected. Some documents may have failed to embed properly. Please try again.", + ) + else: + raise HTTPException( + status_code=500, detail=f"Error preparing retriever: {str(e)}" + ) + + try: + return StreamingResponse( + async_respond, + media_type="text/event-stream", + ) + + except Exception as e_handler: + error_msg = f"Error in streaming chat completion: {str(e_handler)}" + logger.error(error_msg) + raise HTTPException(status_code=500, detail=error_msg) diff --git a/api/routers/codemap.py b/api/routers/codemap.py new file mode 100644 index 000000000..ca4321db6 --- /dev/null +++ b/api/routers/codemap.py @@ -0,0 +1,68 @@ +from fastapi import APIRouter, HTTPException, Query, WebSocket, WebSocketDisconnect +from fastapi.responses import StreamingResponse +from fastapi.websockets import WebSocketState + +from api.logger import get_logger +from api.schemas import CodeMapRequest +from api.services.codemap import generate_codemap, read_repo_file + +logger = get_logger(__name__) + +router = APIRouter(tags=["codemap"]) + + +@router.websocket("/ws/codemap") +async def handle_websocket_codemap(websocket: WebSocket): + """Stream codemap generation events (NDJSON) over a WebSocket.""" + await websocket.accept() + try: + request = CodeMapRequest(**await websocket.receive_json()) + async for event in generate_codemap(request): + if websocket.application_state != WebSocketState.CONNECTED: + break + await websocket.send_text(event) + except WebSocketDisconnect: + logger.info("Codemap WebSocket disconnected") + except Exception as e: # noqa: BLE001 + logger.error("Codemap generation error: %s", str(e), exc_info=True) + if websocket.application_state == WebSocketState.CONNECTED: + import json + + await websocket.send_text( + json.dumps({"type": "error", "message": str(e)}) + "\n" + ) + finally: + if websocket.application_state == WebSocketState.CONNECTED: + await websocket.close() + + +@router.post("/codemap/stream") +async def codemap_stream(request: CodeMapRequest): + """HTTP fallback: stream codemap generation events as NDJSON.""" + try: + return StreamingResponse( + generate_codemap(request), + media_type="application/x-ndjson", + ) + except Exception as e: # noqa: BLE001 + logger.error("Codemap generation error: %s", str(e), exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/codemap/file") +async def codemap_file( + repo_url: str = Query(..., description="Repository URL or local path"), + file_path: str = Query(..., description="Repository-relative file path"), + type: str = Query("github", description="Repository type"), +): + """Return the full content of a file from the cloned/local repository.""" + try: + content = read_repo_file(repo_url, type, file_path) + return {"file_path": file_path, "content": content} + except FileNotFoundError: + raise HTTPException(status_code=404, detail=f"File not found: {file_path}") + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: # noqa: BLE001 + logger.error("Error reading repo file: %s", str(e), exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) diff --git a/api/routers/repo.py b/api/routers/repo.py new file mode 100644 index 000000000..ea79e9db9 --- /dev/null +++ b/api/routers/repo.py @@ -0,0 +1,78 @@ +import asyncio +import json + +from fastapi import APIRouter, Query +from fastapi.responses import StreamingResponse + +from api.logger import get_logger +from api.rag import repo_index_exist +from api.repository import Repo +from api.schemas import RepoPrepareRequest +from api.services.research import prepare_repo_index as prepare_index + +logger = get_logger(__name__) + +router = APIRouter(prefix="/repo", tags=["repo"]) + + +# Heartbeat cadence MUST stay below the frontend proxy / undici bodyTimeout +# (undici default = 300s). 10s keeps the connection alive with wide margin. +_HEARTBEAT_INTERVAL_SEC = 10 + + +@router.post("/prepare") +async def prepare_repo_index(request: RepoPrepareRequest): + async def event_stream(): + # Fast path: cache already warm -> return immediately. + if repo_index_exist(Repo(repo_url=request.repo_url, repo_type=request.type)): + yield "event: ready\ndata: already indexed\n\n" + yield "event: done\ndata: ok\n\n" + return + + # First byte -> response headers are flushed now (kills the timeout). + yield ": indexing-start\n\n" + + task = asyncio.create_task(prepare_index(request)) + elapsed = 0 + # Wait on the task, but wake up every interval to emit a heartbeat. + while not task.done(): + try: + # shield: a heartbeat timeout must NOT cancel the indexing task. + await asyncio.wait_for( + asyncio.shield(task), timeout=_HEARTBEAT_INTERVAL_SEC + ) + except asyncio.TimeoutError: + elapsed += _HEARTBEAT_INTERVAL_SEC + yield f"event: progress\ndata: {json.dumps({'elapsed_sec': elapsed})}\n\n" + except Exception: + # Task raised; break out and report via task.exception() below. + break + + exc = task.exception() + if exc is not None: + logger.error("Repo indexing failed for %s: %s", request.repo_url, exc) + yield f"event: error\ndata: {json.dumps({'error': str(exc)})}\n\n" + else: + logger.info("Repo indexing complete for %s", request.repo_url) + yield "event: done\ndata: ok\n\n" + + return StreamingResponse( + event_stream(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache, no-transform", + "X-Accel-Buffering": "no", # disable proxy buffering (nginx etc.) + }, + ) + + +@router.get("/index/status") +async def repo_index_status( + repo_url: str = Query(..., description="Repository URL or local path"), + type: str = Query("github", description="Repository type"), +): + """Cheap readiness probe: is the embedding index already built on disk? + + Frontend can poll this instead of holding the /repo/prepare stream open. + """ + return {"ready": repo_index_exist(Repo(repo_url=repo_url, repo_type=type))} diff --git a/api/routers/system.py b/api/routers/system.py new file mode 100644 index 000000000..5883f2dab --- /dev/null +++ b/api/routers/system.py @@ -0,0 +1,84 @@ +from datetime import datetime + +from fastapi import APIRouter + +from api.config import configs +from api.logger import get_logger +from api.schemas import Model, ModelConfig, Provider + +logger = get_logger(__name__) + +router = APIRouter(tags=["system"]) + + +@router.get("/health") +async def health_check(): + """Health check endpoint for Docker and monitoring""" + return { + "status": "healthy", + "timestamp": datetime.now().isoformat(), + "service": "deepwiki-api", + } + + +@router.get("/lang/config") +async def lang_config(): + return configs["lang_config"] + + +@router.get("/models/config", response_model=ModelConfig) +async def get_model_config(): + """ + Get available model providers and their models. + + This endpoint returns the configuration of available model providers and their + respective models that can be used throughout the application. + + Returns: + ModelConfig: A configuration object containing providers and their models + """ + try: + logger.info("Fetching model configurations") + + # Create providers from the config file + providers = [] + default_provider = configs.get("default_provider", "google") + + # Add provider configuration based on config.py + for provider_id, provider_config in configs["providers"].items(): + models = [] + # Add models from config + for model_id in provider_config["models"].keys(): + # Get a more user-friendly display name if possible + models.append(Model(id=model_id, name=model_id)) + + # Add provider with its models + providers.append( + Provider( + id=provider_id, + name=f"{provider_id.capitalize()}", + supportsCustomModel=provider_config.get( + "supportsCustomModel", False + ), + models=models, + ) + ) + + # Create and return the full configuration + config = ModelConfig(providers=providers, defaultProvider=default_provider) + return config + + except Exception as e: + logger.error(f"Error creating model configuration: {str(e)}") + # Return some default configuration in case of error + return ModelConfig( + providers=[ + Provider( + id="google", + name="Google", + supportsCustomModel=True, + models=[Model(id="gemini-2.5-flash", name="Gemini 2.5 Flash")], + ) + ], + defaultProvider="google", + ) diff --git a/api/routers/wiki.py b/api/routers/wiki.py new file mode 100644 index 000000000..cf655e667 --- /dev/null +++ b/api/routers/wiki.py @@ -0,0 +1,228 @@ +import os +from datetime import datetime +from typing import Optional + +from fastapi import APIRouter, HTTPException, Query +from fastapi.responses import JSONResponse, Response + +from api.config import WIKI_AUTH_CODE, WIKI_AUTH_MODE, configs +from api.logger import get_logger +from api.schemas import ( + ProcessedProjectEntry, + WikiCacheData, + WikiCacheRequest, + WikiExportRequest, +) +from api.services.wiki import ( + delete_wiki_cache, + export_wiki, + list_processed_projects, + read_wiki_cache, + save_wiki_cache, +) + +logger = get_logger(__name__) + +router = APIRouter(tags=["wiki"]) + + +@router.post("/export/wiki") +async def post_export_wiki(request: WikiExportRequest): + """ + Export wiki content as Markdown or JSON. + + Args: + request: The export request containing wiki pages and format + + Returns: + A downloadable file in the requested format + """ + logger.info(f"Exporting wiki for {request.repo_url} in {request.format} format") + + # Extract repository name from URL for the filename + repo_parts = request.repo_url.rstrip("/").split("/") + repo_name = repo_parts[-1] if len(repo_parts) > 0 else "wiki" + + timestamp = datetime.now() + + content = export_wiki( + request.repo_url, + pages=request.pages, + format=request.format, + timestamp=timestamp, + ) + filename = f"{repo_name}_wiki_{timestamp.strftime('%Y%m%d_%H%M%S')}" + + if request.format == "markdown": + # Generate Markdown content + filename += ".md" + media_type = "text/markdown" + else: # JSON format + # Generate JSON content + filename += ".json" + media_type = "application/json" + + # Create response with appropriate headers for file download + return Response( + content=content, + media_type=media_type, + headers={"Content-Disposition": f"attachment; filename={filename}"}, + ) + + +@router.get("/local_repo/structure") +async def get_local_repo_structure( + path: str = Query(None, description="Path to local repository"), +): + """Return the file tree and README content for a local repository.""" + if not path: + return JSONResponse( + status_code=400, + content={ + "error": "No path provided. Please provide a 'path' query parameter." + }, + ) + + if not os.path.isdir(path): + return JSONResponse( + status_code=404, content={"error": f"Directory not found: {path}"} + ) + + try: + logger.info(f"Processing local repository at: {path}") + file_tree_lines = [] + readme_content = "" + + for root, dirs, files in os.walk(path): + # Exclude hidden dirs/files and virtual envs + dirs[:] = [ + d + for d in dirs + if not d.startswith(".") + and d != "__pycache__" + and d != "node_modules" + and d != ".venv" + ] + for file in files: + if file.startswith(".") or file == "__init__.py" or file == ".DS_Store": + continue + rel_dir = os.path.relpath(root, path) + rel_file = os.path.join(rel_dir, file) if rel_dir != "." else file + file_tree_lines.append(rel_file) + # Find README.md (case-insensitive) + if file.lower() == "readme.md" and not readme_content: + try: + with open(os.path.join(root, file), "r", encoding="utf-8") as f: + readme_content = f.read() + except Exception as e: + logger.warning(f"Could not read README.md: {str(e)}") + readme_content = "" + + file_tree_str = "\n".join(sorted(file_tree_lines)) + return {"file_tree": file_tree_str, "readme": readme_content} + except Exception as e: + logger.error(f"Error processing local repository: {str(e)}") + return JSONResponse( + status_code=500, + content={"error": f"Error processing local repository: {str(e)}"}, + ) + + +@router.get("/api/wiki_cache", response_model=Optional[WikiCacheData]) +async def read_wiki( + owner: str = Query(..., description="Repository owner"), + repo: str = Query(..., description="Repository name"), + repo_type: str = Query(..., description="Repository type (e.g., github, gitlab)"), + language: str = Query(..., description="Language of the wiki content"), +): + """Retrieve cached wiki data (structure and generated pages) for a repository.""" + supported_langs = configs["lang_config"]["supported_languages"] + if language not in supported_langs: + language = configs["lang_config"]["default"] + + logger.info( + f"Attempting to retrieve wiki cache for {owner}/{repo} ({repo_type}), lang: {language}" + ) + cached_data = await read_wiki_cache(owner, repo, repo_type, language) + if cached_data: + return cached_data + # Return 200 with null body if not found (frontend expects this behavior) + logger.info( + f"Wiki cache not found for {owner}/{repo} ({repo_type}), lang: {language}" + ) + return None + + +@router.post("/api/wiki_cache") +async def save_wiki(request_data: WikiCacheRequest): + """ + Stores generated wiki data (structure and pages) to the server-side cache. + """ + # Language validation + supported_langs = configs["lang_config"]["supported_languages"] + + if request_data.language not in supported_langs: + request_data.language = configs["lang_config"]["default"] + + logger.info( + f"Attempting to save wiki cache for {request_data.repo.owner}/{request_data.repo.repo} ({request_data.repo.type}), lang: {request_data.language}" + ) + success = await save_wiki_cache(request_data) + if success: + return {"message": "Wiki cache saved successfully"} + else: + raise HTTPException(status_code=500, detail="Failed to save wiki cache") + + +@router.delete("/api/wiki_cache") +async def delete_wiki( + owner: str = Query(..., description="Repository owner"), + repo: str = Query(..., description="Repository name"), + repo_type: str = Query(..., description="Repository type (e.g., github, gitlab)"), + language: str = Query(..., description="Language of the wiki content"), + authorization_code: Optional[str] = Query(None, description="Authorization code"), +): + """ + Deletes a specific wiki cache from the file system. + """ + # Language validation + supported_langs = configs["lang_config"]["supported_languages"] + if language not in supported_langs: + raise HTTPException(status_code=400, detail="Language is not supported") + + if WIKI_AUTH_MODE: + logger.info("check the authorization code") + if not authorization_code or WIKI_AUTH_CODE != authorization_code: + raise HTTPException(status_code=401, detail="Authorization code is invalid") + + logger.info( + f"Attempting to delete wiki cache for {owner}/{repo} ({repo_type}), lang: {language}" + ) + + try: + deleted = await delete_wiki_cache(owner, repo, repo_type, language) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to delete wiki cache: {str(e)}" + ) + + if deleted: + return { + "message": f"Wiki cache for {owner}/{repo} ({language}) deleted successfully" + } + raise HTTPException(status_code=404, detail="Wiki cache not found") + + +@router.get("/api/processed_projects", response_model=list[ProcessedProjectEntry]) +async def get_processed_projects(): + """ + Lists all processed projects found in the wiki cache directory. + Projects are identified by files named like: deepwiki_cache_{repo_type}_{owner}_{repo}_{language}.json + """ + try: + return await list_processed_projects() + except Exception: + raise HTTPException( + status_code=500, + detail="Failed to list processed projects from server cache.", + ) diff --git a/api/schemas/__init__.py b/api/schemas/__init__.py new file mode 100644 index 000000000..b00c85aa8 --- /dev/null +++ b/api/schemas/__init__.py @@ -0,0 +1,49 @@ +from api.schemas.auth import AuthorizationConfig +from api.schemas.chat import ChatCompletionRequest +from api.schemas.codemap import ( + CodeMap, + CodeMapCitation, + CodeMapRequest, + CodeMapSection, + CodeMapStep, +) +from api.schemas.io import aload, asave +from api.schemas.models import ( + Model, + ModelConfig, + Provider, +) +from api.schemas.repo import RepoInfo, RepoPrepareRequest +from api.schemas.wiki import ( + ProcessedProjectEntry, + WikiCacheData, + WikiCacheRequest, + WikiExportRequest, + WikiPage, + WikiSection, + WikiStructureModel, +) + +__all__ = [ + "AuthorizationConfig", + "ChatCompletionRequest", + "CodeMap", + "CodeMapCitation", + "CodeMapRequest", + "CodeMapSection", + "CodeMapStep", + "Model", + "ModelConfig", + "ProcessedProjectEntry", + "Provider", + "RepoInfo", + "RepoPrepareRequest", + "WikiCacheData", + "WikiCacheRequest", + "WikiExportRequest", + "WikiPage", + "WikiSection", + "WikiStructureModel", + "aload", + "asave", +] diff --git a/api/schemas/auth.py b/api/schemas/auth.py new file mode 100644 index 000000000..57609c46a --- /dev/null +++ b/api/schemas/auth.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel, Field + + +class AuthorizationConfig(BaseModel): + code: str = Field(..., description="Authorization code") diff --git a/api/schemas/base.py b/api/schemas/base.py new file mode 100644 index 000000000..b9632a023 --- /dev/null +++ b/api/schemas/base.py @@ -0,0 +1,44 @@ +from typing import Literal +from urllib.parse import unquote + +from pydantic import BaseModel, Field, field_validator + +RepoType = Literal["local", "github", "gitlab", "bitbucket"] + + +class RepoRequestBase(BaseModel): + repo_url: str = Field(..., description="URL or local path of the repository") + type: RepoType | None = Field("github", description="Repository type") + token: str | None = Field(None, description="PAT for private repositories") + provider: str = Field("google", description="Model provider") + model: str | None = Field(None, description="Model name for the provider") + language: str | None = Field("en", description="Language for content generation") + excluded_dirs: list[str] = Field( + default_factory=list, + description="List or newline-separated string of directories to exclude from processing", + ) + excluded_files: list[str] = Field( + default_factory=list, + description="List or newline-separated string of file patterns to exclude from processing", + ) + included_dirs: list[str] = Field( + default_factory=list, + description="List or newline-separated string of directories to include exclusively", + ) + included_files: list[str] = Field( + default_factory=list, + description="List or newline-separated string of file patterns to include exclusively", + ) + + @field_validator( + "excluded_dirs", + "excluded_files", + "included_dirs", + "included_files", + mode="before", + ) + @classmethod + def validate_path(cls, value: list[str] | str) -> list[str]: + if isinstance(value, str): + value = [unquote(p) for p in value.strip().split("\n") if p] + return value diff --git a/api/schemas/chat.py b/api/schemas/chat.py new file mode 100644 index 000000000..c1be4ce6a --- /dev/null +++ b/api/schemas/chat.py @@ -0,0 +1,29 @@ +from typing import Literal + +from pydantic import BaseModel, Field + +from api.schemas.base import RepoRequestBase + + +# Models for the API +class ChatMessage(BaseModel): + role: str # 'user' or 'assistant' + content: str + mode: Literal["normal", "deep_research"] = Field(default="normal") + + +class ChatCompletionRequest(RepoRequestBase): + """ + Model for requesting a chat completion. + """ + + messages: list[ChatMessage] = Field(..., description="List of chat messages") + filePath: str | None = Field( + None, + description="Optional path to a file in the repository to include in the prompt", + ) + research_iteration: int = Field( + default=1, + ge=1, + description="Current deep research iteration (1-based). Only used when the request is in deep_research mode.", + ) diff --git a/api/schemas/codemap.py b/api/schemas/codemap.py new file mode 100644 index 000000000..2e3e7cf86 --- /dev/null +++ b/api/schemas/codemap.py @@ -0,0 +1,62 @@ +from pydantic import BaseModel, Field + +from api.schemas.base import RepoRequestBase + + +class CodeMapCitation(BaseModel): + """A grounded reference from a codemap step back to real source code.""" + + file_path: str = Field( + ..., description="Repository-relative path of the source file" + ) + start_line: int | None = Field( + None, description="1-based start line of the cited range in the source file" + ) + end_line: int | None = Field( + None, description="1-based end line of the cited range in the source file" + ) + snippet: str = Field( + "", + description="Verbatim excerpt copied from the source, used to locate/verify the range", + ) + + +class CodeMapStep(BaseModel): + """A single actionable sub-step (e.g. 1a, 1b) within a section.""" + + id: str = Field(..., description="Human-facing id such as '1a', '1b', '2a'") + label: str = Field(..., description="Short title of the step") + code: str = Field("", description="Example code snippet illustrating the step") + citation: CodeMapCitation | None = Field( + None, description="Where this step's code comes from in the repository" + ) + + +class CodeMapSection(BaseModel): + """A top-level numbered section grouping related steps.""" + + id: str = Field(..., description="Section id such as '1', '2'") + title: str = Field(..., description="Section title") + guide: str = Field( + "", description="Prose guide for the section (filled in phase 2)" + ) + diagram: str = Field( + "", description="Mermaid diagram source for the section (filled in phase 2)" + ) + steps: list[CodeMapStep] = Field(default_factory=list) + + +class CodeMap(BaseModel): + """The full codemap produced for a user's question.""" + + title: str = Field(..., description="Overall codemap title") + summary: str = Field( + "", description="Introductory summary, may contain [1a][2a] citation markers" + ) + sections: list[CodeMapSection] = Field(default_factory=list) + + +class CodeMapRequest(RepoRequestBase): + """Request to generate a codemap for a repository question.""" + + question: str = Field(..., description="The user's how-to / usage question") diff --git a/api/schemas/io.py b/api/schemas/io.py new file mode 100644 index 000000000..ab539a110 --- /dev/null +++ b/api/schemas/io.py @@ -0,0 +1,20 @@ +from typing import TypeVar + +import anyio +from pydantic import BaseModel + +_M = TypeVar("_M", bound=BaseModel) + + +async def asave(model: BaseModel, path: str, *, encoding: str = "utf-8"): + """Asynchronous serialize and save a model""" + + async with await anyio.open_file(path, mode="w", encoding=encoding) as file: + await file.write(model.model_dump_json()) + + +async def aload(model: type[_M], path: str, *, encoding: str = "utf-8") -> _M: + """Asynchronous deserialize and load a model""" + + async with await anyio.open_file(path, mode="r", encoding=encoding) as file: + return model.model_validate_json(await file.read()) diff --git a/api/schemas/models.py b/api/schemas/models.py new file mode 100644 index 000000000..d05e51dab --- /dev/null +++ b/api/schemas/models.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, Field + + +class Model(BaseModel): + """ + Model for LLM model configuration + """ + + id: str = Field(..., description="Model identifier") + name: str = Field(..., description="Display name for the model") + + +class Provider(BaseModel): + """ + Model for LLM provider configuration + """ + + id: str = Field(..., description="Provider identifier") + name: str = Field(..., description="Display name for the provider") + models: list[Model] = Field( + ..., description="List of available models for this provider" + ) + supportsCustomModel: bool = Field( + False, description="Whether this provider supports custom models" + ) + + +class ModelConfig(BaseModel): + """ + Model for the entire model configuration + """ + + providers: list[Provider] = Field( + ..., description="List of available model providers" + ) + defaultProvider: str = Field(..., description="ID of the default provider") diff --git a/api/schemas/repo.py b/api/schemas/repo.py new file mode 100644 index 000000000..a47dcd0f6 --- /dev/null +++ b/api/schemas/repo.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel + +from api.schemas.base import RepoRequestBase + + +class RepoPrepareRequest(RepoRequestBase): + """Request body for POST /repo/prepare (index warming). No chat messages.""" + + +class RepoInfo(BaseModel): + owner: str + repo: str + type: str + token: str | None = None + localPath: str | None = None + repoUrl: str | None = None diff --git a/api/schemas/wiki.py b/api/schemas/wiki.py new file mode 100644 index 000000000..8ffb6bfa2 --- /dev/null +++ b/api/schemas/wiki.py @@ -0,0 +1,90 @@ +from typing import Literal + +from pydantic import BaseModel, Field + +from api.schemas.repo import RepoInfo + + +class WikiPage(BaseModel): + """ + Model for a wiki page. + """ + + id: str + title: str + content: str + filePaths: list[str] + importance: str # Should ideally be Literal['high', 'medium', 'low'] + relatedPages: list[str] + + +class WikiSection(BaseModel): + """ + Model for the wiki sections. + """ + + id: str + title: str + pages: list[str] + subsections: list[str] | None = None + + +class WikiStructureModel(BaseModel): + """ + Model for the overall wiki structure. + """ + + id: str + title: str + description: str + pages: list[WikiPage] + sections: list[WikiSection] | None = None + rootSections: list[str] | None = None + + +class WikiCacheData(BaseModel): + """ + Model for the data to be stored in the wiki cache. + """ + + wiki_structure: WikiStructureModel + generated_pages: dict[str, WikiPage] + repo_url: str | None = None # compatible for old cache + repo: RepoInfo | None = None + provider: str | None = None + model: str | None = None + + +class WikiCacheRequest(BaseModel): + """ + Model for the request body when saving wiki cache. + """ + + repo: RepoInfo + language: str + wiki_structure: WikiStructureModel + generated_pages: dict[str, WikiPage] + provider: str + model: str + + +class WikiExportRequest(BaseModel): + """ + Model for requesting a wiki export. + """ + + repo_url: str = Field(..., description="URL of the repository") + pages: list[WikiPage] = Field(..., description="List of wiki pages to export") + format: Literal["markdown", "json"] = Field( + ..., description="Export format (markdown or json)" + ) + + +class ProcessedProjectEntry(BaseModel): + id: str # Filename + owner: str + repo: str + name: str # owner/repo + repo_type: str # Renamed from type to repo_type for clarity with existing models + submittedAt: int # Timestamp + language: str # Extracted from filename diff --git a/api/services/__init__.py b/api/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/services/codemap.py b/api/services/codemap.py new file mode 100644 index 000000000..8c36a313b --- /dev/null +++ b/api/services/codemap.py @@ -0,0 +1,312 @@ +"""Codemap generation pipeline. + +Produces a structured, source-grounded "codemap" for a user's usage/how-to question +using a two-call LLM flow: + + 1. skeleton — analyze code + generate the initial codemap (sections, steps, citations) + 2. enrich — fill in per-section prose guides and mermaid diagrams + +Results are streamed to the client as newline-delimited JSON (NDJSON) events so the UI +can render the three-phase progress (analyzing -> initial codemap -> diagrams/guides). +""" + +import asyncio +import json +import os +import re +from collections.abc import AsyncIterator, Callable + +from api.chat import ChatStreamer, prompt_builder +from api.config import get_model_config +from api.logger import get_logger +from api.prompts import CODEMAP_ENRICH_PROMPT, CODEMAP_SKELETON_PROMPT +from api.rag import RAG +from api.schemas import CodeMap, CodeMapRequest + +logger = get_logger(__name__) + + +def _event(**payload) -> str: + """Serialize one NDJSON event line.""" + return json.dumps(payload, ensure_ascii=False) + "\n" + + +def _phase(phase: str, status: str, **extra) -> str: + return _event(type="phase", phase=phase, status=status, **extra) + + +async def _collect_stream(streamer: ChatStreamer, prompt: str) -> str: + """Run a streaming completion to the end and return the full text.""" + parts: list[str] = [] + async for chunk in streamer.respond_stream(prompt): + parts.append(chunk) + return "".join(parts) + + +async def _generate_json( + streamer_factory: Callable[[], ChatStreamer], + prompt: str, + attempts: int = 3, +) -> dict: + """Collect a completion and parse JSON, retrying on malformed output. + + A fresh streamer is used per attempt so nondeterministic JSON glitches from + smaller local models get another chance to come out valid. + """ + last_error: Exception | None = None + for attempt in range(1, attempts + 1): + raw = await _collect_stream(streamer_factory(), prompt) + try: + return _extract_json(raw) + except Exception as e: # noqa: BLE001 + last_error = e + logger.warning("JSON parse attempt %d/%d failed: %s", attempt, attempts, e) + raise ValueError( + f"Model did not return valid JSON after {attempts} attempts: {last_error}" + ) + + +def _repair_json(candidate: str) -> str: + """Fix a few common LLM JSON glitches (trailing commas, stray quote+space + before a key such as ``" "id":``).""" + repaired = re.sub(r",\s*([}\]])", r"\1", candidate) # trailing commas + repaired = re.sub(r'"\s+"(\w+)"\s*:', r'"\1":', repaired) # `" "key":` -> `"key":` + return repaired + + +def _extract_json(text: str) -> dict: + """Best-effort extraction of a single JSON object from model output. + + Handles ```json fences and leading/trailing prose by scanning for the first + balanced top-level object, with a light repair pass for common glitches. + """ + if not text: + raise ValueError("Empty model response") + + # Strip common code fences. + cleaned = text.strip() + if cleaned.startswith("```"): + # remove opening fence (```json or ```) and trailing fence + cleaned = cleaned.split("\n", 1)[-1] + if cleaned.rstrip().endswith("```"): + cleaned = cleaned.rstrip()[:-3] + + # Isolate the first balanced top-level object (ignoring braces inside strings). + start = cleaned.find("{") + if start == -1: + raise ValueError("No JSON object found in model response") + depth = 0 + in_str = False + escape = False + candidate = cleaned[start:] + for i in range(start, len(cleaned)): + ch = cleaned[i] + if in_str: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == '"': + in_str = False + continue + if ch == '"': + in_str = True + elif ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + candidate = cleaned[start : i + 1] + break + + try: + return json.loads(candidate) + except json.JSONDecodeError: + return json.loads(_repair_json(candidate)) # may still raise -> caller retries + + +def _format_context(documents: list) -> str: + """Group retrieved chunks by file and annotate each with its real line range.""" + docs_by_file: dict[str, list] = {} + for doc in documents: + file_path = doc.meta_data.get("file_path", "unknown") + docs_by_file.setdefault(file_path, []).append(doc) + + context_parts = [] + for file_path, docs in docs_by_file.items(): + chunk_texts = [] + for doc in docs: + start_line = doc.meta_data.get("start_line") + end_line = doc.meta_data.get("end_line") + if start_line and end_line: + chunk_texts.append(f"[lines {start_line}-{end_line}]\n{doc.text}") + else: + chunk_texts.append(doc.text) + header = f"## File Path: {file_path}\n\n" + context_parts.append(header + "\n\n".join(chunk_texts)) + + return "\n\n" + ("-" * 10) + "\n\n".join(context_parts) + + +def local_repo_dir(repo_url: str, repo_type: str | None) -> str: + """Resolve the on-disk directory of a cloned/local repository.""" + from adalflow.utils import get_adalflow_default_root_path + + repo_url = repo_url.strip() + if repo_url.startswith("http://") or repo_url.startswith("https://"): + parts = repo_url.rstrip("/").split("/") + if repo_type in ("github", "gitlab", "bitbucket") and len(parts) >= 5: + name = f"{parts[-2]}_{parts[-1].replace('.git', '')}" + else: + name = parts[-1].replace(".git", "") + return os.path.join(get_adalflow_default_root_path(), "repos", name) + return repo_url # already a local path + + +def read_repo_file(repo_url: str, repo_type: str | None, file_path: str) -> str: + """Read a file from the cloned/local repository, guarding against traversal.""" + repo_dir = os.path.realpath(local_repo_dir(repo_url, repo_type)) + target = os.path.realpath(os.path.join(repo_dir, file_path)) + if os.path.commonpath([repo_dir, target]) != repo_dir: + raise ValueError("Resolved path escapes the repository directory") + if not os.path.isfile(target): + raise FileNotFoundError(file_path) + with open(target, "r", encoding="utf-8", errors="replace") as f: + return f.read() + + +def _locate_snippet(text: str, snippet: str) -> tuple[int, int] | None: + """Find the 1-based line range of ``snippet`` inside ``text``. + + LLM-provided line numbers are unreliable, but the snippet is copied verbatim, + so the true location is recovered by searching the real file. + """ + snippet = snippet.strip("\n") + if not snippet: + return None + pos = text.find(snippet) + if pos != -1: + start = text.count("\n", 0, pos) + 1 + return start, start + snippet.count("\n") + # Fallback: anchor on the first non-blank line of the snippet. + first = next((ln.strip() for ln in snippet.splitlines() if ln.strip()), "") + if first: + idx = text.find(first) + if idx != -1: + start = text.count("\n", 0, idx) + 1 + return start, start + snippet.count("\n") + return None + + +def _ground_citations(codemap: CodeMap, repo_dir: str) -> None: + """Overwrite each citation's line range with the true snippet location in the + cloned source file, so the code viewer highlights the right lines.""" + file_cache: dict[str, str | None] = {} + for section in codemap.sections: + for step in section.steps: + cit = step.citation + if not cit or not cit.snippet or not cit.file_path: + continue + if cit.file_path not in file_cache: + path = os.path.join(repo_dir, cit.file_path) + try: + with open(path, "r", encoding="utf-8") as f: + file_cache[cit.file_path] = f.read() + except (OSError, UnicodeDecodeError): + file_cache[cit.file_path] = None + text = file_cache[cit.file_path] + if not text: + continue + loc = _locate_snippet(text, cit.snippet) + if loc: + cit.start_line, cit.end_line = loc + + +async def generate_codemap(request: CodeMapRequest) -> AsyncIterator[str]: + """Generate a codemap, yielding NDJSON progress + result events.""" + repo_url = request.repo_url + repo_name = repo_url.rstrip("/").split("/")[-1] if "/" in repo_url else repo_url + repo_type = request.type + + prompt_fmt = { + "repo_type": repo_type, + "repo_url": repo_url, + "repo_name": repo_name, + "language_name": request.language or "en", + } + + # ---- Phase 1a: analyzing code (RAG retrieval) --------------------------------- + yield _phase("analyzing", "start") + rag = await asyncio.to_thread(RAG, provider=request.provider, model=request.model) + await rag.aprepare_retriever( + request.repo_url, + request.type, + request.token, + excluded_files=request.excluded_files, + excluded_dirs=request.excluded_dirs, + included_files=request.included_files, + included_dirs=request.included_dirs, + ) + retrieved = await rag.acall(request.question, language=request.language or "en") + documents = retrieved[0].documents if retrieved and retrieved[0].documents else [] + logger.info("Codemap retrieval returned %d chunks", len(documents)) + context_text = _format_context(documents) if documents else "" + yield _phase("analyzing", "done", chunk_count=len(documents)) + + model_config = get_model_config(request.provider, request.model)["model_kwargs"] + + def _new_streamer() -> ChatStreamer: + return ChatStreamer.create( + provider=request.provider, + model=request.model, + model_config=model_config, + ) + + # ---- Phase 1b: initial codemap skeleton --------------------------------------- + yield _phase("initial_codemap", "start") + skeleton_prompt = prompt_builder( + system_prompt=CODEMAP_SKELETON_PROMPT.format(**prompt_fmt), + query=request.question, + context=context_text, + ) + try: + skeleton = CodeMap.model_validate( + await _generate_json(_new_streamer, skeleton_prompt) + ) + except Exception as e: # noqa: BLE001 + logger.error("Failed to parse codemap skeleton: %s", e) + yield _event(type="error", stage="initial_codemap", message=str(e)) + return + yield _phase("initial_codemap", "done", section_count=len(skeleton.sections)) + + # ---- Phase 2: diagrams and guides --------------------------------------------- + yield _phase("diagrams", "start") + enrich_query = ( + f"{request.question}\n\n\n" + f"{skeleton.model_dump_json()}\n" + ) + enrich_prompt = prompt_builder( + system_prompt=CODEMAP_ENRICH_PROMPT.format(**prompt_fmt), + query=enrich_query, + context=context_text, + ) + final = skeleton + try: + final = CodeMap.model_validate( + await _generate_json(_new_streamer, enrich_prompt, attempts=2) + ) + yield _phase("diagrams", "done") + except Exception as e: # noqa: BLE001 + # Non-fatal: fall back to the skeleton (no diagrams/guides) so the user + # still gets a usable codemap. + logger.warning("Diagram/guide enrichment failed, using skeleton: %s", e) + yield _phase("diagrams", "done", degraded=True) + + # Ground citation line numbers against the real source (LLM line numbers are + # guesses; the verbatim snippet is authoritative). + repo_dir = (getattr(rag.db_manager, "repo_paths", None) or {}).get("save_repo_dir") + if repo_dir: + _ground_citations(final, repo_dir) + + yield _event(type="codemap", data=final.model_dump()) + yield _event(type="done") diff --git a/api/services/research.py b/api/services/research.py new file mode 100644 index 000000000..66a709d4e --- /dev/null +++ b/api/services/research.py @@ -0,0 +1,334 @@ +import asyncio +from collections.abc import AsyncIterator, Callable +from functools import partial + +from api.chat import ChatStreamer, is_token_limit_error, prompt_builder +from api.config import configs, get_model_config +from api.logger import get_logger +from api.prompts import ( + DEEP_RESEARCH_FINAL_ITERATION_PROMPT, + DEEP_RESEARCH_FIRST_ITERATION_PROMPT, + DEEP_RESEARCH_INTERMEDIATE_ITERATION_PROMPT, + SIMPLE_CHAT_SYSTEM_PROMPT, +) +from api.rag import RAG, count_tokens, repo_index_exist +from api.repository import Repo, get_repo_content +from api.schemas import ChatCompletionRequest, RepoPrepareRequest + +logger = get_logger(__name__) + + +# Maximum token limit for embedding models +MAX_INPUT_TOKENS = 7500 # Safe threshold below 8192 token limit + + +class RepoNotIndexedError(ValueError): + """Raised when a chat request arrives before the repo has been indexed.""" + + +async def prepare_repo_index( + request: ChatCompletionRequest | RepoPrepareRequest, +) -> RAG: + rag = await asyncio.to_thread( + RAG, + provider=request.provider, + model=request.model, + ) + # Extract custom file filter parameters if provided + if request.excluded_dirs: + logger.info(f"Using custom excluded directories: {request.excluded_dirs}") + if request.excluded_files: + logger.info(f"Using custom excluded files: {request.excluded_files}") + if request.included_dirs: + logger.info(f"Using custom included directories: {request.included_dirs}") + if request.included_files: + logger.info(f"Using custom included files: {request.included_files}") + + await rag.aprepare_retriever( + request.repo_url, + request.type, + request.token, + excluded_files=request.excluded_files, + excluded_dirs=request.excluded_dirs, + included_files=request.included_files, + included_dirs=request.included_dirs, + ) + return rag + + +async def research_chat( + request: ChatCompletionRequest, +) -> AsyncIterator[str]: + input_too_large = False + if request.messages and len(request.messages) > 0: + last_message = request.messages[-1] + if hasattr(last_message, "content") and last_message.content: + tokens = count_tokens(last_message.content, embedder_type=request.provider) + logger.info(f"Request size: {tokens} tokens") + if tokens > MAX_INPUT_TOKENS: + logger.warning( + f"Request exceeds recommended token limit ({tokens} > {MAX_INPUT_TOKENS})" + ) + input_too_large = True + + repo = Repo(repo_url=request.repo_url, repo_type=request.type) + if not repo_index_exist(repo=repo): + logger.warning( + "Repo %s is not indexed yet. Call `/repo/prepare` first if encounter Timeout", + repo.name, + ) + + try: + rag = await prepare_repo_index(request=request) + logger.info("Retriever prepared for %s", request.repo_url) + + except ValueError as e: + if "No valid documents with embeddings found" in str(e): + logger.error(f"No valid embeddings found: {str(e)}") + raise e + else: + logger.error("ValueError preparing retriever: %s", str(e)) + raise e + except Exception as e: + logger.error("Error preparing retriever: %s", str(e)) + raise e + + if not request.messages: + raise ValueError("No messages provided") + + last_message = request.messages[-1] + is_deep_research = last_message.mode == "deep_research" + + if last_message.role != "user": + raise ValueError("Last message must be from the user") + + # Process previous messages to build conversation history + for i in range(0, len(request.messages) - 1, 2): + if i + 1 < len(request.messages): + user_msg = request.messages[i] + assistant_msg = request.messages[i + 1] + + if user_msg.role == "user" and assistant_msg.role == "assistant": + rag.memory.add_dialog_turn( + user_query=user_msg.content, + assistant_response=assistant_msg.content, + ) + + if is_deep_research: + logger.info( + "Deep Research request detected - iteration %d", request.research_iteration + ) + + # Check if this is a continuation request + if ( + "continue" in last_message.content.lower() + and "research" in last_message.content.lower() + ): + # Find the original topic from the first user message + original_topic = None + for msg in request.messages: + if msg.role == "user" and "continue" not in msg.content.lower(): + original_topic = msg.content.strip() + logger.info(f"Found original research topic: {original_topic}") + break + + if original_topic: + # Replace the continuation message with the original topic + last_message.content = original_topic + logger.info(f"Using original topic for research: {original_topic}") + + # Get the query from the last message + query = last_message.content + + # Only retrieve documents if input is not too large + context_text = "" + + if not input_too_large: + try: + # If filePath exists, modify the query for RAG to focus on the file + rag_query = query + if request.filePath: + # Use the file path to get relevant context about the file + rag_query = f"Contexts related to {request.filePath}" + logger.info(f"Modified RAG query to focus on file: {request.filePath}") + + # Try to perform RAG retrieval + try: + # This will use the actual RAG implementation + retrieved_documents = await rag.acall( + rag_query, language=request.language + ) + + if retrieved_documents and retrieved_documents[0].documents: + # Format context for the prompt in a more structured way + documents = retrieved_documents[0].documents + logger.info(f"Retrieved {len(documents)} documents") + + # Group documents by file path + docs_by_file = {} + for doc in documents: + file_path = doc.meta_data.get("file_path", "unknown") + if file_path not in docs_by_file: + docs_by_file[file_path] = [] + docs_by_file[file_path].append(doc) + + # Format context text with file path grouping + context_parts = [] + for file_path, docs in docs_by_file.items(): + # Add file header with metadata + header = f"## File Path: {file_path}\n\n" + # Add document content, annotating each chunk with its + # real line range when available so the model can cite lines. + chunk_texts = [] + for doc in docs: + start_line = doc.meta_data.get("start_line") + end_line = doc.meta_data.get("end_line") + if start_line and end_line: + chunk_texts.append( + f"[lines {start_line}-{end_line}]\n{doc.text}" + ) + else: + chunk_texts.append(doc.text) + content = "\n\n".join(chunk_texts) + + context_parts.append(f"{header}{content}") + + # Join all parts with clear separation + context_text = "\n\n" + "-" * 10 + "\n\n".join(context_parts) + else: + logger.warning("No documents retrieved from RAG") + except Exception as e: + logger.error(f"Error in RAG retrieval: {str(e)}") + except Exception as e: + logger.error(f"Error retrieving documents: {str(e)}") + context_text = "" + + # Get repository information + repo_url = request.repo_url + repo_name = repo_url.split("/")[-1] if "/" in repo_url else repo_url + + # Determine repository type + repo_type = request.type + + # Get language information + language_code = request.language or configs["lang_config"]["default"] + supported_langs = configs["lang_config"]["supported_languages"] + language_name = supported_langs.get(language_code, "English") + + # Create system prompt + if is_deep_research: + # Check if this is the first iteration + is_first_iteration = request.research_iteration == 1 + + # Check if this is the final iteration + is_final_iteration = request.research_iteration >= 5 + + if is_first_iteration: + system_prompt = DEEP_RESEARCH_FIRST_ITERATION_PROMPT.format( + repo_type=repo_type, + repo_url=repo_url, + repo_name=repo_name, + language_name=language_name, + ) + elif is_final_iteration: + system_prompt = DEEP_RESEARCH_FINAL_ITERATION_PROMPT.format( + repo_type=repo_type, + repo_url=repo_url, + repo_name=repo_name, + language_name=language_name, + ) + else: + system_prompt = DEEP_RESEARCH_INTERMEDIATE_ITERATION_PROMPT.format( + repo_type=repo_type, + repo_url=repo_url, + repo_name=repo_name, + research_iteration=request.research_iteration, + language_name=language_name, + ) + else: + system_prompt = SIMPLE_CHAT_SYSTEM_PROMPT.format( + repo_type=repo_type, + repo_url=repo_url, + repo_name=repo_name, + language_name=language_name, + ) + + # Fetch file content if provided + file_content = "" + if request.filePath: + try: + file_content = await asyncio.to_thread( + get_repo_content, + repo_url=request.repo_url, + file_path=request.filePath, + repo_type=request.type, + access_token=request.token, + ) + logger.info(f"Successfully retrieved content for file: {request.filePath}") + except Exception as e: + logger.error(f"Error retrieving file content: {str(e)}") + # Continue without file content if there's an error + + # Format conversation history + conversation_history = "" + for turn_id, turn in rag.memory().items(): + if ( + not isinstance(turn_id, int) + and hasattr(turn, "user_query") + and hasattr(turn, "assistant_response") + ): + conversation_history += f"\n{turn.user_query.query_str}\n{turn.assistant_response.response_str}\n\n" + + async def stream_and_fallback( + streamer: ChatStreamer, + prompt_func: Callable[[], str], + simplified_prompt_func: Callable[[], str], + ) -> AsyncIterator[str]: + try: + async for chunk in streamer.respond_stream(prompt_func()): + yield chunk + except Exception as e: + if is_token_limit_error(e): + logger.warning("Token limit exceeded, retrying without context") + try: + async for chunk in streamer.respond_stream( + simplified_prompt_func() + ): + yield chunk + except Exception as e2: + logger.error("Error in fallback streaming response: %s", str(e2)) + yield ( + "\nI apologize, but your request is too large for me to process. " + "Please try a shorter query or break it into smaller parts." + ) + else: + error_str = f"Error with {streamer.provider} API: {e}" + logger.error(error_str, exc_info=True) + if streamer.error_hint: + error_str += f"\n\n{streamer.error_hint}" + yield "\n" + error_str + + model_config = get_model_config(request.provider, request.model)["model_kwargs"] + chat_streamer = ChatStreamer.create( + provider=request.provider, + model=request.model, + model_config=model_config, + ) + + prompt_kwargs = { + "system_prompt": system_prompt, + "query": query, + "conversation_history": conversation_history, + "file_path": request.filePath, + "file_content": file_content, + "context": context_text, + } + + prompt_func = partial(prompt_builder, **prompt_kwargs, simplify=False) + simplified_prompt_func = partial(prompt_builder, **prompt_kwargs, simplify=True) + + return stream_and_fallback( + streamer=chat_streamer, + prompt_func=prompt_func, + simplified_prompt_func=simplified_prompt_func, + ) diff --git a/api/services/wiki.py b/api/services/wiki.py new file mode 100644 index 000000000..0d7ae434b --- /dev/null +++ b/api/services/wiki.py @@ -0,0 +1,232 @@ +import asyncio +import json +import os +from datetime import datetime +from typing import Literal + +from api.logger import get_logger +from api.schemas import ( + ProcessedProjectEntry, + WikiCacheData, + WikiCacheRequest, + WikiPage, + aload, + asave, +) + +logger = get_logger(__name__) + + +# Helper function to get adalflow root path +def get_adalflow_default_root_path(): + return os.path.expanduser(os.path.join("~", ".adalflow")) + + +WIKI_CACHE_DIR = os.path.join(get_adalflow_default_root_path(), "wikicache") +os.makedirs(WIKI_CACHE_DIR, exist_ok=True) + + +def get_wiki_cache_path(owner: str, repo: str, repo_type: str, language: str) -> str: + """Generates the file path for a given wiki cache.""" + filename = f"deepwiki_cache_{repo_type}_{owner}_{repo}_{language}.json" + return os.path.join(WIKI_CACHE_DIR, filename) + + +async def read_wiki_cache( + owner: str, repo: str, repo_type: str, language: str +) -> WikiCacheData | None: + """Reads wiki cache data from the file system.""" + cache_path = get_wiki_cache_path(owner, repo, repo_type, language) + if not os.path.exists(cache_path): + return None + try: + return await aload(WikiCacheData, cache_path, encoding="utf-8") + except Exception: + logger.exception("Error reading wiki cache from %s", cache_path) + return None + + +async def save_wiki_cache(data: WikiCacheRequest) -> bool: + """Saves wiki cache data to the file system.""" + cache_path = get_wiki_cache_path( + data.repo.owner, data.repo.repo, data.repo.type, data.language + ) + logger.info(f"Attempting to save wiki cache. Path: {cache_path}") + try: + wiki_cache = WikiCacheData( + wiki_structure=data.wiki_structure, + generated_pages=data.generated_pages, + repo=data.repo, + provider=data.provider, + model=data.model, + ) + await asave(wiki_cache, cache_path, encoding="utf-8") + logger.info(f"Wiki cache successfully saved to {cache_path}") + return True + except OSError: + logger.exception("IOError saving wiki cache to %s", cache_path) + return False + except Exception: + logger.exception("Unexpected error saving wiki cache to %s", cache_path) + return False + + +async def delete_wiki_cache(owner: str, repo: str, repo_type: str, language: str): + cache_path = get_wiki_cache_path( + owner, + repo, + repo_type, + language, + ) + + if not os.path.exists(cache_path): + logger.warning("Wiki cache not found, cannot delete: %s", cache_path) + return False + + os.remove(cache_path) + logger.info("Successfully deleted wiki cache: %s", cache_path) + return True + + +async def list_processed_projects() -> list[ProcessedProjectEntry]: + project_entries: list[ProcessedProjectEntry] = [] + + if not os.path.exists(WIKI_CACHE_DIR): + logger.info( + f"Cache directory {WIKI_CACHE_DIR} not found. Returning empty list." + ) + return [] + + logger.info(f"Scanning for project cache files in: {WIKI_CACHE_DIR}") + filenames = await asyncio.to_thread(os.listdir, WIKI_CACHE_DIR) + + for filename in filenames: + if filename.startswith("deepwiki_cache_") and filename.endswith(".json"): + file_path = os.path.join(WIKI_CACHE_DIR, filename) + try: + stats = await asyncio.to_thread(os.stat, file_path) + parts = ( + filename.replace("deepwiki_cache_", "") + .replace(".json", "") + .split("_") + ) + # Expecting repo_type_owner_repo_language + if len(parts) >= 4: + repo_type = parts[0] + owner = parts[1] + language = parts[-1] + repo = "_".join(parts[2:-1]) # repo can contain underscores + project_entries.append( + ProcessedProjectEntry( + id=filename, + owner=owner, + repo=repo, + name=f"{owner}/{repo}", + repo_type=repo_type, + submittedAt=int(stats.st_mtime * 1000), + language=language, + ) + ) + else: + logger.warning( + f"Could not parse project details from filename: {filename}" + ) + except Exception as e: + logger.error(f"Error processing file {file_path}: {e}") + continue + + project_entries.sort(key=lambda p: p.submittedAt, reverse=True) + logger.info(f"Found {len(project_entries)} processed project entries.") + return project_entries + + +def _generate_json_export( + repo_url: str, pages: list[WikiPage], timestamp: datetime +) -> str: + """ + Generate JSON export of wiki pages. + + Args: + repo_url: The repository URL + pages: List of wiki pages + + Returns: + JSON content as string + """ + # Create a dictionary with metadata and pages + export_data = { + "metadata": { + "repository": repo_url, + "generated_at": timestamp.isoformat(), + "page_count": len(pages), + }, + "pages": [page.model_dump() for page in pages], + } + + # Convert to JSON string with pretty formatting + return json.dumps(export_data, indent=2) + + +def _generate_markdown_export( + repo_url: str, pages: list[WikiPage], timestamp: datetime +) -> str: + """ + Generate Markdown export of wiki pages. + + Args: + repo_url: The repository URL + pages: List of wiki pages + + Returns: + Markdown content as string + """ + # Start with metadata + markdown = f"# Wiki Documentation for {repo_url}\n\n" + markdown += f"Generated on: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}\n\n" + + # Add table of contents + markdown += "## Table of Contents\n\n" + for page in pages: + markdown += f"- [{page.title}](#{page.id})\n" + markdown += "\n" + + # Add each page + for page in pages: + markdown += f"\n\n" + markdown += f"## {page.title}\n\n" + + # Add related pages + if page.relatedPages and len(page.relatedPages) > 0: + markdown += "### Related Pages\n\n" + related_titles = [] + for related_id in page.relatedPages: + # Find the title of the related page + related_page = next((p for p in pages if p.id == related_id), None) + if related_page: + related_titles.append(f"[{related_page.title}](#{related_id})") + + if related_titles: + markdown += "Related topics: " + ", ".join(related_titles) + "\n\n" + + # Add page content + markdown += f"{page.content}\n\n" + markdown += "---\n\n" + + return markdown + + +def export_wiki( + repo_url: str, + pages: list[WikiPage], + format: Literal["json", "markdown"], + timestamp: datetime | None = None, +) -> str: + dt = timestamp or datetime.now() + if format == "json": + return _generate_json_export(repo_url, pages, timestamp=dt) + elif format == "markdown": + return _generate_markdown_export(repo_url, pages, timestamp=dt) + else: + raise NotImplementedError( + f"Exporting wiki to format {format} is not supported. Must be one of 'markdown' or 'json'.", + ) diff --git a/api/simple_chat.py b/api/simple_chat.py deleted file mode 100644 index a6e7e8aae..000000000 --- a/api/simple_chat.py +++ /dev/null @@ -1,345 +0,0 @@ -import asyncio -import logging -from typing import Callable -from functools import partial - -from fastapi import FastAPI, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import StreamingResponse - -from api.chat import ChatStreamer, prompt_builder, is_token_limit_error -from api.config import get_model_config, configs -from api.data_pipeline import count_tokens, get_file_content -from api.rag import RAG, MAX_INPUT_TOKENS -from api.prompts import ( - DEEP_RESEARCH_FIRST_ITERATION_PROMPT, - DEEP_RESEARCH_FINAL_ITERATION_PROMPT, - DEEP_RESEARCH_INTERMEDIATE_ITERATION_PROMPT, - SIMPLE_CHAT_SYSTEM_PROMPT -) -from api.chat_model import ChatCompletionRequest - -# Configure logging -from api.logging_config import setup_logging - -setup_logging() -logger = logging.getLogger(__name__) - - -# Initialize FastAPI app -app = FastAPI( - title="Simple Chat API", - description="Simplified API for streaming chat completions" -) - -# Configure CORS -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # Allows all origins - allow_credentials=True, - allow_methods=["*"], # Allows all methods - allow_headers=["*"], # Allows all headers -) - - -@app.post("/chat/completions/stream") -async def chat_completions_stream(request: ChatCompletionRequest): - """Stream a chat completion response directly using Google Generative AI""" - try: - # Check if request contains very large input - input_too_large = False - if request.messages and len(request.messages) > 0: - last_message = request.messages[-1] - if hasattr(last_message, 'content') and last_message.content: - tokens = count_tokens(last_message.content, embedder_type=request.provider) - logger.info(f"Request size: {tokens} tokens") - if tokens > MAX_INPUT_TOKENS: - logger.warning(f"Request exceeds recommended token limit ({tokens} > {MAX_INPUT_TOKENS})") - input_too_large = True - - # Create a new RAG instance for this request - try: - # async do RAG initialization - request_rag = await asyncio.to_thread( - RAG, - provider=request.provider, - model=request.model, - ) - - # Extract custom file filter parameters if provided - if request.excluded_dirs: - logger.info(f"Using custom excluded directories: {request.excluded_dirs}") - if request.excluded_files: - logger.info(f"Using custom excluded files: {request.excluded_files}") - if request.included_dirs: - logger.info(f"Using custom included directories: {request.included_dirs}") - if request.included_files: - logger.info(f"Using custom included files: {request.included_files}") - - await request_rag.aprepare_retriever( - request.repo_url, - request.type, - request.token, - excluded_dirs=request.excluded_dirs, - excluded_files=request.excluded_files, - included_dirs=request.included_dirs, - included_files=request.included_files, - ) - logger.info(f"Retriever prepared for {request.repo_url}") - except ValueError as e: - if "No valid documents with embeddings found" in str(e): - logger.error(f"No valid embeddings found: {str(e)}") - raise HTTPException(status_code=500, detail="No valid document embeddings found. This may be due to embedding size inconsistencies or API errors during document processing. Please try again or check your repository content.") - else: - logger.error(f"ValueError preparing retriever: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error preparing retriever: {str(e)}") - except Exception as e: - logger.error(f"Error preparing retriever: {str(e)}") - # Check for specific embedding-related errors - if "All embeddings should be of the same size" in str(e): - raise HTTPException(status_code=500, detail="Inconsistent embedding sizes detected. Some documents may have failed to embed properly. Please try again.") - else: - raise HTTPException(status_code=500, detail=f"Error preparing retriever: {str(e)}") - - # Validate request - if not request.messages or len(request.messages) == 0: - raise HTTPException(status_code=400, detail="No messages provided") - - last_message = request.messages[-1] - if last_message.role != "user": - raise HTTPException(status_code=400, detail="Last message must be from the user") - - # Process previous messages to build conversation history - for i in range(0, len(request.messages) - 1, 2): - if i + 1 < len(request.messages): - user_msg = request.messages[i] - assistant_msg = request.messages[i + 1] - - if user_msg.role == "user" and assistant_msg.role == "assistant": - request_rag.memory.add_dialog_turn( - user_query=user_msg.content, - assistant_response=assistant_msg.content - ) - - # Check if this is a Deep Research request - is_deep_research = last_message.mode == "deep_research" - - # Count research iterations if this is a Deep Research request - if is_deep_research: - logger.info("Deep Research request detected - iteration %d",request.research_iteration) - - # Check if this is a continuation request - if "continue" in last_message.content.lower() and "research" in last_message.content.lower(): - # Find the original topic from the first user message - original_topic = None - for msg in request.messages: - if msg.role == "user" and "continue" not in msg.content.lower(): - original_topic = msg.content.strip() - logger.info(f"Found original research topic: {original_topic}") - break - - if original_topic: - # Replace the continuation message with the original topic - last_message.content = original_topic - logger.info(f"Using original topic for research: {original_topic}") - - # Get the query from the last message - query = last_message.content - - # Only retrieve documents if input is not too large - context_text = "" - retrieved_documents = None - - if not input_too_large: - try: - # If filePath exists, modify the query for RAG to focus on the file - rag_query = query - if request.filePath: - # Use the file path to get relevant context about the file - rag_query = f"Contexts related to {request.filePath}" - logger.info(f"Modified RAG query to focus on file: {request.filePath}") - - # Try to perform RAG retrieval - try: - # This will use the actual RAG implementation - retrieved_documents = await request_rag.acall(rag_query, language=request.language) - - if retrieved_documents and retrieved_documents[0].documents: - # Format context for the prompt in a more structured way - documents = retrieved_documents[0].documents - logger.info(f"Retrieved {len(documents)} documents") - - # Group documents by file path - docs_by_file = {} - for doc in documents: - file_path = doc.meta_data.get('file_path', 'unknown') - if file_path not in docs_by_file: - docs_by_file[file_path] = [] - docs_by_file[file_path].append(doc) - - # Format context text with file path grouping - context_parts = [] - for file_path, docs in docs_by_file.items(): - # Add file header with metadata - header = f"## File Path: {file_path}\n\n" - # Add document content - content = "\n\n".join([doc.text for doc in docs]) - - context_parts.append(f"{header}{content}") - - # Join all parts with clear separation - context_text = "\n\n" + "-" * 10 + "\n\n".join(context_parts) - else: - logger.warning("No documents retrieved from RAG") - except Exception as e: - logger.error(f"Error in RAG retrieval: {str(e)}") - # Continue without RAG if there's an error - - except Exception as e: - logger.error(f"Error retrieving documents: {str(e)}") - context_text = "" - - # Get repository information - repo_url = request.repo_url - repo_name = repo_url.split("/")[-1] if "/" in repo_url else repo_url - - # Determine repository type - repo_type = request.type - - # Get language information - language_code = request.language or configs["lang_config"]["default"] - supported_langs = configs["lang_config"]["supported_languages"] - language_name = supported_langs.get(language_code, "English") - - # Create system prompt - if is_deep_research: - # Check if this is the first iteration - is_first_iteration = request.research_iteration == 1 - - # Check if this is the final iteration - is_final_iteration = request.research_iteration >= 5 - - if is_first_iteration: - system_prompt = DEEP_RESEARCH_FIRST_ITERATION_PROMPT.format( - repo_type=repo_type, - repo_url=repo_url, - repo_name=repo_name, - language_name=language_name - ) - elif is_final_iteration: - system_prompt = DEEP_RESEARCH_FINAL_ITERATION_PROMPT.format( - repo_type=repo_type, - repo_url=repo_url, - repo_name=repo_name, - language_name=language_name - ) - else: - system_prompt = DEEP_RESEARCH_INTERMEDIATE_ITERATION_PROMPT.format( - repo_type=repo_type, - repo_url=repo_url, - repo_name=repo_name, - research_iteration=request.research_iteration, - language_name=language_name - ) - else: - system_prompt = SIMPLE_CHAT_SYSTEM_PROMPT.format( - repo_type=repo_type, - repo_url=repo_url, - repo_name=repo_name, - language_name=language_name - ) - - # Fetch file content if provided - file_content = "" - if request.filePath: - try: - file_content = await asyncio.to_thread( - get_file_content, - repo_url=request.repo_url, - file_path=request.filePath, - repo_type=request.type, - access_token=request.token, - ) - logger.info(f"Successfully retrieved content for file: {request.filePath}") - except Exception as e: - logger.error(f"Error retrieving file content: {str(e)}") - # Continue without file content if there's an error - - # Format conversation history - conversation_history = "" - for turn_id, turn in request_rag.memory().items(): - if not isinstance(turn_id, int) and hasattr(turn, 'user_query') and hasattr(turn, 'assistant_response'): - conversation_history += f"\n{turn.user_query.query_str}\n{turn.assistant_response.response_str}\n\n" - - async def stream_and_fallback( - streamer: ChatStreamer, - prompt_func: Callable[[], str], - simplified_prompt_func: Callable[[], str], - ): - try: - async for chunk in streamer.respond_stream(prompt_func()): - yield chunk - except Exception as e: - if is_token_limit_error(e): - logger.warning("Token limit exceeded, retrying without context") - try: - async for chunk in streamer.respond_stream(simplified_prompt_func()): - yield chunk - except Exception as e2: - logger.error("Error in fallback streaming response: %s", str(e2)) - yield ( - f"\nI apologize, but your request is too large for me to process. " - f"Please try a shorter query or break it into smaller parts." - ) - else: - error_str = f"Error with {streamer.provider} API: {e}" - logger.error(error_str, exc_info=True) - if streamer.error_hint: - error_str += f"\n\n{streamer.error_hint}" - yield "\n" + error_str - - model_config = get_model_config(request.provider, request.model)["model_kwargs"] - chat_streamer = ChatStreamer.create( - provider=request.provider, - model=request.model, - model_config=model_config, - ) - prompt_kwargs = dict( - system_prompt=system_prompt, - query=query, - conversation_history=conversation_history, - file_path=request.filePath, - file_content=file_content, - context=context_text, - ) - - prompt_func = partial( - prompt_builder, - **prompt_kwargs, - simplify=False, - ) - simplified_prompt_func = partial( - prompt_builder, - **prompt_kwargs, - simplify=True, - ) - - # Return streaming response - return StreamingResponse(stream_and_fallback( - streamer=chat_streamer, - prompt_func=prompt_func, - simplified_prompt_func=simplified_prompt_func, - ), media_type="text/event-stream") - - except HTTPException: - raise - except Exception as e_handler: - error_msg = f"Error in streaming chat completion: {str(e_handler)}" - logger.error(error_msg) - raise HTTPException(status_code=500, detail=error_msg) - - -@app.get("/") -async def root(): - """Root endpoint to check if the API is running""" - return {"status": "API is running", "message": "Navigate to /docs for API documentation"} diff --git a/api/tools/embedder.py b/api/tools/embedder.py deleted file mode 100644 index 050d63547..000000000 --- a/api/tools/embedder.py +++ /dev/null @@ -1,58 +0,0 @@ -import adalflow as adal - -from api.config import configs, get_embedder_type - - -def get_embedder(is_local_ollama: bool = False, use_google_embedder: bool = False, embedder_type: str = None) -> adal.Embedder: - """Get embedder based on configuration or parameters. - - Args: - is_local_ollama: Legacy parameter for Ollama embedder - use_google_embedder: Legacy parameter for Google embedder - embedder_type: Direct specification of embedder type ('ollama', 'google', 'bedrock', 'openai') - - Returns: - adal.Embedder: Configured embedder instance - """ - # Determine which embedder config to use - if embedder_type: - if embedder_type == 'ollama': - embedder_config = configs["embedder_ollama"] - elif embedder_type == 'google': - embedder_config = configs["embedder_google"] - elif embedder_type == 'bedrock': - embedder_config = configs["embedder_bedrock"] - else: # default to openai - embedder_config = configs["embedder"] - elif is_local_ollama: - embedder_config = configs["embedder_ollama"] - elif use_google_embedder: - embedder_config = configs["embedder_google"] - else: - # Auto-detect based on current configuration - current_type = get_embedder_type() - if current_type == 'bedrock': - embedder_config = configs["embedder_bedrock"] - elif current_type == 'ollama': - embedder_config = configs["embedder_ollama"] - elif current_type == 'google': - embedder_config = configs["embedder_google"] - else: - embedder_config = configs["embedder"] - - # --- Initialize Embedder --- - model_client_class = embedder_config["model_client"] - if "initialize_kwargs" in embedder_config: - model_client = model_client_class(**embedder_config["initialize_kwargs"]) - else: - model_client = model_client_class() - - # Create embedder with basic parameters - embedder_kwargs = {"model_client": model_client, "model_kwargs": embedder_config["model_kwargs"]} - - embedder = adal.Embedder(**embedder_kwargs) - - # Set batch_size as an attribute if available (not a constructor parameter) - if "batch_size" in embedder_config: - embedder.batch_size = embedder_config["batch_size"] - return embedder diff --git a/api/websocket_wiki.py b/api/websocket_wiki.py deleted file mode 100644 index d72efc2b5..000000000 --- a/api/websocket_wiki.py +++ /dev/null @@ -1,328 +0,0 @@ -import asyncio -import logging -from collections.abc import AsyncIterator, Callable -from functools import partial - -from fastapi import WebSocket, WebSocketDisconnect - -from api.chat import ChatStreamer, prompt_builder, is_token_limit_error -from api.config import ( - get_model_config, - configs, -) -from api.data_pipeline import count_tokens, get_file_content -from api.rag import RAG, MAX_INPUT_TOKENS -from api.prompts import ( - DEEP_RESEARCH_FIRST_ITERATION_PROMPT, - DEEP_RESEARCH_FINAL_ITERATION_PROMPT, - DEEP_RESEARCH_INTERMEDIATE_ITERATION_PROMPT, - SIMPLE_CHAT_SYSTEM_PROMPT, -) -from api.chat_model import ChatCompletionRequest - -# Configure logging -from api.logging_config import setup_logging - -setup_logging() -logger = logging.getLogger(__name__) - - -async def handle_websocket_chat(websocket: WebSocket): - """ - Handle WebSocket connection for chat completions. - This replaces the HTTP streaming endpoint with a WebSocket connection. - """ - await websocket.accept() - - try: - # Receive and parse the request data - request_data = await websocket.receive_json() - request = ChatCompletionRequest(**request_data) - - # Check if request contains very large input - input_too_large = False - if request.messages and len(request.messages) > 0: - last_message = request.messages[-1] - if hasattr(last_message, 'content') and last_message.content: - tokens = count_tokens(last_message.content, embedder_type=request.provider) - logger.info(f"Request size: {tokens} tokens") - if tokens > MAX_INPUT_TOKENS: - logger.warning(f"Request exceeds recommended token limit ({tokens} > {MAX_INPUT_TOKENS})") - input_too_large = True - - # Create a new RAG instance for this request - try: - request_rag = await asyncio.to_thread( - RAG, - provider=request.provider, - model=request.model, - ) - - # Extract custom file filter parameters if provided - if request.excluded_dirs: - logger.info(f"Using custom excluded directories: {request.excluded_dirs}") - if request.excluded_files: - logger.info(f"Using custom excluded files: {request.excluded_files}") - if request.included_dirs: - logger.info(f"Using custom included directories: {request.included_dirs}") - if request.included_files: - logger.info(f"Using custom included files: {request.included_files}") - - await request_rag.aprepare_retriever( - request.repo_url, - request.type, - request.token, - excluded_dirs=request.excluded_dirs, - excluded_files=request.excluded_files, - included_dirs=request.included_dirs, - included_files=request.included_files, - ) - logger.info(f"Retriever prepared for {request.repo_url}") - except ValueError as e: - if "No valid documents with embeddings found" in str(e): - logger.error(f"No valid embeddings found: {str(e)}") - await websocket.send_text("Error: No valid document embeddings found. This may be due to embedding size inconsistencies or API errors during document processing. Please try again or check your repository content.") - return - else: - logger.error(f"ValueError preparing retriever: {str(e)}") - await websocket.send_text(f"Error preparing retriever: {str(e)}") - return - except Exception as e: - logger.error(f"Error preparing retriever: {str(e)}") - # Check for specific embedding-related errors - if "All embeddings should be of the same size" in str(e): - await websocket.send_text("Error: Inconsistent embedding sizes detected. Some documents may have failed to embed properly. Please try again.") - else: - await websocket.send_text(f"Error preparing retriever: {str(e)}") - return - - # Validate request - if not request.messages or len(request.messages) == 0: - await websocket.send_text("Error: No messages provided") - return - - last_message = request.messages[-1] - if last_message.role != "user": - await websocket.send_text("Error: Last message must be from the user") - return - - # Process previous messages to build conversation history - for i in range(0, len(request.messages) - 1, 2): - if i + 1 < len(request.messages): - user_msg = request.messages[i] - assistant_msg = request.messages[i + 1] - - if user_msg.role == "user" and assistant_msg.role == "assistant": - request_rag.memory.add_dialog_turn( - user_query=user_msg.content, - assistant_response=assistant_msg.content - ) - - # Check if this is a Deep Research request - is_deep_research = last_message.mode == "deep_research" - - # Count research iterations if this is a Deep Research request - if is_deep_research: - logger.info("Deep Research request detected - iteration %d", request.research_iteration) - - # Check if this is a continuation request - if "continue" in last_message.content.lower() and "research" in last_message.content.lower(): - # Find the original topic from the first user message - original_topic = None - for msg in request.messages: - if msg.role == "user" and "continue" not in msg.content.lower(): - original_topic = msg.content.strip() - logger.info(f"Found original research topic: {original_topic}") - break - - if original_topic: - # Replace the continuation message with the original topic - last_message.content = original_topic - logger.info(f"Using original topic for research: {original_topic}") - - # Get the query from the last message - query = last_message.content - - # Only retrieve documents if input is not too large - context_text = "" - retrieved_documents = None - - if not input_too_large: - try: - # If filePath exists, modify the query for RAG to focus on the file - rag_query = query - if request.filePath: - # Use the file path to get relevant context about the file - rag_query = f"Contexts related to {request.filePath}" - logger.info(f"Modified RAG query to focus on file: {request.filePath}") - - # Try to perform RAG retrieval - try: - # This will use the actual RAG implementation - retrieved_documents = await request_rag.acall(rag_query, language=request.language) - - if retrieved_documents and retrieved_documents[0].documents: - # Format context for the prompt in a more structured way - documents = retrieved_documents[0].documents - logger.info(f"Retrieved {len(documents)} documents") - - # Group documents by file path - docs_by_file = {} - for doc in documents: - file_path = doc.meta_data.get('file_path', 'unknown') - if file_path not in docs_by_file: - docs_by_file[file_path] = [] - docs_by_file[file_path].append(doc) - - # Format context text with file path grouping - context_parts = [] - for file_path, docs in docs_by_file.items(): - # Add file header with metadata - header = f"## File Path: {file_path}\n\n" - # Add document content - content = "\n\n".join([doc.text for doc in docs]) - - context_parts.append(f"{header}{content}") - - # Join all parts with clear separation - context_text = "\n\n" + "-" * 10 + "\n\n".join(context_parts) - else: - logger.warning("No documents retrieved from RAG") - except Exception as e: - logger.error(f"Error in RAG retrieval: {str(e)}") - # Continue without RAG if there's an error - - except Exception as e: - logger.error(f"Error retrieving documents: {str(e)}") - context_text = "" - - # Get repository information - repo_url = request.repo_url - repo_name = repo_url.split("/")[-1] if "/" in repo_url else repo_url - - # Determine repository type - repo_type = request.type - - # Get language information - language_code = request.language or configs["lang_config"]["default"] - supported_langs = configs["lang_config"]["supported_languages"] - language_name = supported_langs.get(language_code, "English") - - # Create system prompt - if is_deep_research: - # Check if this is the first iteration - is_first_iteration = request.research_iteration == 1 - - # Check if this is the final iteration - is_final_iteration = request.research_iteration >= 5 - - if is_first_iteration: - system_prompt = DEEP_RESEARCH_FIRST_ITERATION_PROMPT.format( - repo_type=repo_type, - repo_url=repo_url, - repo_name=repo_name, - language_name=language_name - ) - elif is_final_iteration: - system_prompt = DEEP_RESEARCH_FINAL_ITERATION_PROMPT.format( - repo_type=repo_type, - repo_url=repo_url, - repo_name=repo_name, - language_name=language_name - ) - else: - system_prompt = DEEP_RESEARCH_INTERMEDIATE_ITERATION_PROMPT.format( - repo_type=repo_type, - repo_url=repo_url, - repo_name=repo_name, - research_iteration=request.research_iteration, - language_name=language_name - ) - else: - system_prompt = SIMPLE_CHAT_SYSTEM_PROMPT.format( - repo_type=repo_type, - repo_url=repo_url, - repo_name=repo_name, - language_name=language_name - ) - - # Fetch file content if provided - file_content = "" - if request.filePath: - try: - file_content = await asyncio.to_thread( - get_file_content, - repo_url=request.repo_url, - file_path=request.filePath, - repo_type=request.type, - access_token=request.token, - ) - logger.info(f"Successfully retrieved content for file: {request.filePath}") - except Exception as e: - logger.error(f"Error retrieving file content: {str(e)}") - # Continue without file content if there's an error - - # Format conversation history - conversation_history = "" - for turn_id, turn in request_rag.memory().items(): - if not isinstance(turn_id, int) and hasattr(turn, 'user_query') and hasattr(turn, 'assistant_response'): - conversation_history += f"\n{turn.user_query.query_str}\n{turn.assistant_response.response_str}\n\n" - - async def stream_and_fallback( - streamer: ChatStreamer, - prompt_func: Callable[[], str], - simplified_prompt_func: Callable[[], str], - ) -> AsyncIterator[str]: - try: - async for chunk in streamer.respond_stream(prompt_func()): - yield chunk - except Exception as e: - if is_token_limit_error(e): - logger.warning("Token limit exceeded, retrying without context") - try: - async for chunk in streamer.respond_stream(simplified_prompt_func()): - yield chunk - except Exception as e2: - logger.error("Error in fallback streaming response: %s", e2) - yield ( - "\nI apologize, but your request is too large for me to process. " - "Please try a shorter query or break it into smaller parts." - ) - else: - msg = f"Error with {streamer.provider} API: {e}" - logger.error(msg, exc_info=True) - if streamer.error_hint: - msg += f"\n\n{streamer.error_hint}" - yield "\n" + msg - - model_config = get_model_config(request.provider, request.model)["model_kwargs"] - chat_streamer = ChatStreamer.create( - provider=request.provider, - model=request.model, - model_config=model_config, - ) - - prompt_kwargs = dict( - system_prompt=system_prompt, - query=query, - conversation_history=conversation_history, - file_path=request.filePath, - file_content=file_content, - context=context_text, - ) - prompt_func = partial(prompt_builder, **prompt_kwargs, simplify=False) - simplified_prompt_func = partial(prompt_builder, **prompt_kwargs, simplify=True) - - async for chunk in stream_and_fallback(chat_streamer, prompt_func, simplified_prompt_func): - await websocket.send_text(chunk) - - except WebSocketDisconnect: - logger.info("WebSocket disconnected") - except Exception as e: - logger.error(f"Error in WebSocket handler: {str(e)}") - try: - await websocket.send_text(f"Error: {str(e)}") - except Exception: - pass - finally: - await websocket.close() diff --git a/docker-compose-litellm.env b/docker-compose-litellm.env index 0a246ec4e..f6e8f53a8 100644 --- a/docker-compose-litellm.env +++ b/docker-compose-litellm.env @@ -4,4 +4,4 @@ LITELLM_BASE_URL=http://litellm:4000 # Use env. variable LITELLM_API_KEY # But if it doesn't exist, default to `sk-1234` -LITELLM_API_KEY=${LITELLM_API_KEY:-sk-1234} \ No newline at end of file +LITELLM_API_KEY=${LITELLM_API_KEY:-sk-1234} diff --git a/docker-compose-litellm.yml b/docker-compose-litellm.yml index 6f90878a2..b28d5e405 100644 --- a/docker-compose-litellm.yml +++ b/docker-compose-litellm.yml @@ -67,4 +67,4 @@ services: - litellm volumes: - deepwiki_litellm_db: \ No newline at end of file + deepwiki_litellm_db: diff --git a/package-lock.json b/package-lock.json index 1e1dfe9a5..c63a1fcc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "0.1.0", "dependencies": { "mermaid": "^11.4.1", - "next": "15.3.1", + "next": "^15.5.22", "next-intl": "^4.1.0", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.5.0", "react-markdown": "^10.1.0", - "react-syntax-highlighter": "^15.6.1", + "react-syntax-highlighter": "^16.1.1", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", @@ -24,14 +24,14 @@ "svg-pan-zoom": "^3.6.2" }, "devDependencies": { - "@eslint/eslintrc": "^3", + "@eslint/eslintrc": "^0.1.0", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "@types/react-syntax-highlighter": "^15.5.13", - "eslint": "^9", - "eslint-config-next": "15.3.1", + "eslint": "^10.8.0", + "eslint-config-next": "^16.2.12", "tailwindcss": "^4", "typescript": "^5" } @@ -53,7 +53,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", - "license": "MIT", "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" @@ -62,108 +61,301 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@antfu/utils": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", - "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", - "license": "MIT", + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, "funding": { - "url": "https://github.com/sponsors/antfu" + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "license": "MIT", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "engines": { "node": ">=6.9.0" } }, - "node_modules/@braintree/sanitize-url": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", - "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", - "license": "MIT" + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", - "license": "Apache-2.0", + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, "dependencies": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", - "license": "Apache-2.0", + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, "dependencies": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", - "license": "Apache-2.0" + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==" }, "node_modules/@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", - "license": "Apache-2.0" + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==" }, "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.4", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", - "license": "MIT", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, - "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -191,172 +383,110 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, - "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, - "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.0.tgz", + "integrity": "sha512-bfL5365QSCmH6cPeFT7Ywclj8C7LiF7sO6mUGzZhtAMV7iID1Euq6740u/SRi4C80NOnVz/CEfK8/HO+nCAPJg==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", + "debug": "^4.1.1", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, - "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@formatjs/ecma402-abstract": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz", - "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==", - "license": "MIT", - "dependencies": { - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/intl-localematcher": "0.6.1", - "decimal.js": "^10.4.3", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", - "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@formatjs/fast-memoize": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", - "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", + "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==" }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz", - "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==", - "license": "MIT", + "version": "3.5.15", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.15.tgz", + "integrity": "sha512-5o4grXKotAB3JqQuisLApHG43g17N+paoRTa92Jiz35Zvfemq0cVf4EDvuxyHAzmsJji7igaEowicLO/VmfJ8Q==", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.4", - "@formatjs/icu-skeleton-parser": "1.8.14", - "tslib": "^2.8.0" + "@formatjs/icu-skeleton-parser": "2.1.11" } }, "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.14", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz", - "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.4", - "tslib": "^2.8.0" - } + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==" }, "node_modules/@formatjs/intl-localematcher": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", - "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", - "license": "MIT", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.13.tgz", + "integrity": "sha512-kHEAFOkeJSPNi7c5PaKaRjxcBrJwzzt81ifUu+8uve1EDW/VJl83KsxmqgqNZLzcFEhSliZGvx3+pk/RH0IOmg==", "dependencies": { - "tslib": "2" + "@formatjs/fast-memoize": "3.1.7" } }, "node_modules/@humanfs/core": { @@ -428,45 +558,34 @@ "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT" + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==" }, "node_modules/@iconify/utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", - "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", - "license": "MIT", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", "dependencies": { - "@antfu/install-pkg": "^1.0.0", - "@antfu/utils": "^8.1.0", + "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", - "debug": "^4.4.0", - "globals": "^15.14.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.0.0", - "mlly": "^1.7.4" + "import-meta-resolve": "^4.2.0" } }, - "node_modules/@iconify/utils/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "license": "MIT", + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "optional": true, "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], - "license": "Apache-2.0", "optional": true, "os": [ "darwin" @@ -478,17 +597,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], - "license": "Apache-2.0", "optional": true, "os": [ "darwin" @@ -500,17 +618,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], - "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" @@ -520,13 +637,12 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], - "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" @@ -536,13 +652,12 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], - "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -552,13 +667,12 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], - "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -568,13 +682,27 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], - "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], "optional": true, "os": [ "linux" @@ -584,13 +712,12 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], - "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -600,13 +727,12 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], - "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -616,13 +742,12 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], - "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -632,13 +757,12 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], - "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -648,13 +772,12 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], - "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -666,17 +789,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], - "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -688,17 +810,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ "ppc64" ], - "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -710,17 +831,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" + "@img/sharp-libvips-linux-ppc64": "1.2.4" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", "cpu": [ - "s390x" + "riscv64" ], - "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -732,17 +852,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ - "x64" + "s390x" ], - "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -754,17 +873,37 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ - "arm64" + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" ], - "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -776,17 +915,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], - "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -798,20 +936,19 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.4" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -821,13 +958,12 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" @@ -840,13 +976,12 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" @@ -859,13 +994,12 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" @@ -941,12 +1075,11 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz", - "integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==", - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", "dependencies": { - "langium": "3.3.1" + "@chevrotain/types": "~11.1.2" } }, "node_modules/@napi-rs/wasm-runtime": { @@ -963,29 +1096,26 @@ } }, "node_modules/@next/env": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz", - "integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==", - "license": "MIT" + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.22.tgz", + "integrity": "sha512-O5BlKb3KtsHkvO0gjjV66PuJnAgCtIEIzwkt50HRAHsQkU1t77eksIXSZV84/WMtZJjWrnDUPKHVRi0D62nSAA==" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.1.tgz", - "integrity": "sha512-oEs4dsfM6iyER3jTzMm4kDSbrQJq8wZw5fmT6fg2V3SMo+kgG+cShzLfEV20senZzv8VF+puNLheiGPlBGsv2A==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.12.tgz", + "integrity": "sha512-uF2z/qAK2q7B5/6CpnFcBRX6jOq5iCO+Uqh1UkJhXljX1JwLarLYhhoJadO6dPb6moTprOKewMXheBcbIoSbug==", "dev": true, - "license": "MIT", "dependencies": { "fast-glob": "3.3.1" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz", - "integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==", + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.22.tgz", + "integrity": "sha512-/VISwtffSg8+fVvBbXdglsvruCsdbBC4dG25iU6xascKVqfQKsj/OtjGnOEkIS7pX5GB9e9/r5QprpicsGL3gw==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -995,13 +1125,12 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz", - "integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==", + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.22.tgz", + "integrity": "sha512-NiA9ve8hbiuhG/Q17a2mZDRVxMTtg3rTOgjLnDaLlE+AEPAQlkkuKrfePEbeOrgYmX0U2KGX4EVEn09hXU5GlQ==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1011,13 +1140,12 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz", - "integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==", + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.22.tgz", + "integrity": "sha512-vAPa9vltW+UW/KWtjXeSUFgV3wb1x9d/BeyC6WFI6eBpL0D2f70oGwtOp6193mNW3qusrpgBzMQferPf+Zh8Dw==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1027,13 +1155,12 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz", - "integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==", + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.22.tgz", + "integrity": "sha512-iknK80pWlNDnkdSr13bd8mMuG3Z2oTxODwsZHvuMY7caMk77+rBLdHVWsy8v2EVa3ZojJ/+wJX5fnq8va6Gv8A==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1043,13 +1170,12 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz", - "integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==", + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.22.tgz", + "integrity": "sha512-penuEdkwU2OOAiS+n4LE8T/VIoCfAI01QcLZTJ2xc3+l4Q22L/DzURocmI2LU1b+8BMQoLAP1Sze3uYAZT05Bg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1059,13 +1185,12 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz", - "integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==", + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.22.tgz", + "integrity": "sha512-ZM0BKJm3FZ+guG6WT6PcyOLtp6paZ5tngcJC/uUKvLW4Y0TQnnVi1+UGdo8Q6Yxp5gaS82pmC1rD/oFlhkWB3g==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1075,13 +1200,12 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz", - "integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==", + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.22.tgz", + "integrity": "sha512-rY/YaumrZaS0//94BnHLF5VSRp0GFUO4GvXNuoCBb0cGSci96yO+p1JaNL2aq9YZAYv9cuZRziV02x5IQH/wjg==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -1091,13 +1215,12 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz", - "integrity": "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==", + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.22.tgz", + "integrity": "sha512-s5IA4cyrbR2XK/5NWcu5dp8CfPBiKME+UhvNperia7uQybEgg5+LIhGMiY37WQE4rcI4owsDcU4IVUjLoTuDkA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -1141,38 +1264,480 @@ "fastq": "^1.6.0" }, "engines": { - "node": ">= 8" + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "hasInstallScript": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==" + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.46.tgz", + "integrity": "sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.46.tgz", + "integrity": "sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.46.tgz", + "integrity": "sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.46.tgz", + "integrity": "sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.46.tgz", + "integrity": "sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.46.tgz", + "integrity": "sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.46.tgz", + "integrity": "sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.46.tgz", + "integrity": "sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.46.tgz", + "integrity": "sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.46.tgz", + "integrity": "sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.46.tgz", + "integrity": "sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.4.0" + "node": ">=10" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", - "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@schummar/icu-type-parser": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", - "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", - "license": "MIT" + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.46.tgz", + "integrity": "sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } }, "node_modules/@swc/counter": { "version": "0.1.3", @@ -1189,6 +1754,14 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", @@ -1466,11 +2039,10 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -1738,6 +2310,12 @@ "@types/ms": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1772,15 +2350,13 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/katex": { "version": "0.16.8", @@ -1813,11 +2389,15 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==" + }, "node_modules/@types/react": { "version": "19.1.11", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz", "integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1847,7 +2427,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", "optional": true }, "node_modules/@types/unist": { @@ -1857,21 +2436,19 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", - "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, - "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/type-utils": "8.40.0", - "@typescript-eslint/utils": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1881,33 +2458,31 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.40.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", - "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1917,20 +2492,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", - "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.40.0", - "@typescript-eslint/types": "^8.40.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1940,18 +2514,17 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", - "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1962,11 +2535,10 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", - "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1975,21 +2547,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", - "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1999,16 +2570,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", - "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2018,22 +2588,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", - "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.40.0", - "@typescript-eslint/tsconfig-utils": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2043,76 +2611,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", - "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, - "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2122,19 +2633,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", - "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2151,279 +2661,338 @@ "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", "cpu": [ "wasm32" ], "dev": true, - "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2436,17 +3005,15 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2458,35 +3025,11 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">= 0.4" } @@ -2536,7 +3079,6 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -2557,7 +3099,6 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -2617,7 +3158,6 @@ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -2655,8 +3195,7 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/async-function": { "version": "1.0.0", @@ -2685,11 +3224,10 @@ } }, "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", "dev": true, - "license": "MPL-2.0", "engines": { "node": ">=4" } @@ -2699,7 +3237,6 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">= 0.4" } @@ -2718,18 +3255,39 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.4.tgz", + "integrity": "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==", "dev": true, - "license": "MIT" + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "dev": true, - "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -2745,27 +3303,48 @@ "node": ">=8" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "streamsearch": "^1.1.0" + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": ">=10.16.0" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -2817,9 +3396,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001737", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", - "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "funding": [ { "type": "opencollective", @@ -2833,34 +3412,16 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/character-entities": { @@ -2903,32 +3464,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/chevrotain-allstar": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", - "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", - "license": "MIT", - "dependencies": { - "lodash-es": "^4.17.21" - }, - "peerDependencies": { - "chevrotain": "^11.0.0" - } - }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -2945,51 +3480,6 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -3004,7 +3494,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", "engines": { "node": ">= 10" } @@ -3013,14 +3502,13 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" + "dev": true }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "license": "MIT" + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, "node_modules/cose-base": { "version": "1.0.3", @@ -3050,14 +3538,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/cytoscape": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", - "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", - "license": "MIT", + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "engines": { "node": ">=0.10" } @@ -3105,7 +3591,6 @@ "version": "7.9.0", "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", "dependencies": { "d3-array": "3", "d3-axis": "3", @@ -3146,7 +3631,6 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", "dependencies": { "internmap": "1 - 2" }, @@ -3158,7 +3642,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", "engines": { "node": ">=12" } @@ -3167,7 +3650,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", @@ -3183,7 +3665,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", "dependencies": { "d3-path": "1 - 3" }, @@ -3195,7 +3676,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", "engines": { "node": ">=12" } @@ -3204,7 +3684,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", "dependencies": { "d3-array": "^3.2.0" }, @@ -3216,7 +3695,6 @@ "version": "6.0.4", "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", "dependencies": { "delaunator": "5" }, @@ -3228,7 +3706,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", "engines": { "node": ">=12" } @@ -3237,7 +3714,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" @@ -3250,7 +3726,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", "dependencies": { "commander": "7", "iconv-lite": "0.6", @@ -3275,7 +3750,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", "engines": { "node": ">=12" } @@ -3284,7 +3758,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", "dependencies": { "d3-dsv": "1 - 3" }, @@ -3296,7 +3769,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", @@ -3307,10 +3779,9 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "engines": { "node": ">=12" } @@ -3319,7 +3790,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", "dependencies": { "d3-array": "2.5.0 - 3" }, @@ -3331,7 +3801,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", "engines": { "node": ">=12" } @@ -3340,7 +3809,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", "dependencies": { "d3-color": "1 - 3" }, @@ -3352,7 +3820,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", "engines": { "node": ">=12" } @@ -3361,7 +3828,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", "engines": { "node": ">=12" } @@ -3370,7 +3836,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", "engines": { "node": ">=12" } @@ -3379,7 +3844,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", "engines": { "node": ">=12" } @@ -3428,7 +3892,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", @@ -3444,7 +3907,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" @@ -3457,7 +3919,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", "engines": { "node": ">=12" } @@ -3466,7 +3927,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", "dependencies": { "d3-path": "^3.1.0" }, @@ -3478,7 +3938,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", "dependencies": { "d3-array": "2 - 3" }, @@ -3490,7 +3949,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", "dependencies": { "d3-time": "1 - 3" }, @@ -3502,7 +3960,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", "engines": { "node": ">=12" } @@ -3511,7 +3968,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", @@ -3530,7 +3986,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", @@ -3543,10 +3998,9 @@ } }, "node_modules/dagre-d3-es": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", - "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", - "license": "MIT", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" @@ -3556,8 +4010,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" + "dev": true }, "node_modules/data-view-buffer": { "version": "1.0.2", @@ -3614,16 +4067,14 @@ } }, "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": { "ms": "^2.1.3" }, @@ -3636,12 +4087,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -3699,10 +4144,9 @@ } }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "dependencies": { "robust-predicates": "^3.0.2" } @@ -3717,11 +4161,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "devOptional": true, - "license": "Apache-2.0", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "engines": { "node": ">=8" } @@ -3753,10 +4195,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", - "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", - "license": "(MPL-2.0 OR Apache-2.0)", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -3776,12 +4217,17 @@ "node": ">= 0.4" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", + "dev": true + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/enhanced-resolve": { "version": "5.18.3", @@ -3810,11 +4256,10 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, - "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", @@ -3899,28 +4344,27 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.2", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3986,6 +4430,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4000,34 +4458,29 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "dev": true, - "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -4037,8 +4490,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4046,7 +4498,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -4061,25 +4513,23 @@ } }, "node_modules/eslint-config-next": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.3.1.tgz", - "integrity": "sha512-GnmyVd9TE/Ihe3RrvcafFhXErErtr2jS0JDeCSp3vWvy86AXwHsRBt0E3MqP/m8ACS1ivcsi5uaqjbhsG18qKw==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.12.tgz", + "integrity": "sha512-iaaf4vvKo5h2LBdGt0JuRv7t0Ysqr9FMCiFxbptDg8LqOE//mIKR80DdpOnSVM7qjLH3jT8P0aFiwXxBEGZRXw==", "dev": true, - "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.3.1", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@next/eslint-plugin-next": "16.2.12", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" }, "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "peerDependenciesMeta": { @@ -4115,7 +4565,6 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", "dev": true, - "license": "ISC", "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", @@ -4146,11 +4595,10 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -4168,7 +4616,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -4178,7 +4625,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, - "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4207,22 +4653,42 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -4232,7 +4698,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, - "license": "MIT", "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -4257,12 +4722,33 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, - "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -4291,32 +4777,65 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, - "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", "dev": true, - "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4326,65 +4845,62 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -4397,7 +4913,6 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -4435,12 +4950,6 @@ "node": ">=0.10.0" } }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "license": "MIT" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -4459,7 +4968,6 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -4476,7 +4984,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -4579,11 +5086,10 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "dev": true }, "node_modules/for-each": { "version": "0.3.5", @@ -4650,6 +5156,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4708,11 +5223,10 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, - "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -4734,11 +5248,10 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -4783,13 +5296,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/hachure-fill": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", @@ -4809,16 +5315,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -4878,57 +5374,26 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, - "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-from-dom": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", - "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", - "license": "ISC", - "dependencies": { - "@types/hast": "^3.0.0", - "hastscript": "^9.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-dom/node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.4" } }, - "node_modules/hast-util-from-dom/node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", - "license": "MIT", + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", "dependencies": { "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" }, "funding": { "type": "opencollective", @@ -4989,36 +5454,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-from-parse5/node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5/node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/hast-util-is-element": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", @@ -5033,10 +5468,12 @@ } }, "node_modules/hast-util-parse-selector": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", - "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", - "license": "MIT", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dependencies": { + "@types/hast": "^3.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -5153,68 +5590,34 @@ } }, "node_modules/hastscript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", - "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", - "license": "MIT", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/hastscript/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/hastscript/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/hastscript/node_modules/comma-separated-tokens": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true }, - "node_modules/hastscript/node_modules/property-information": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", - "license": "MIT", + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, "dependencies": { - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "hermes-estree": "0.25.1" } }, "node_modules/highlight.js": { @@ -5256,7 +5659,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -5264,6 +5666,20 @@ "node": ">=0.10.0" } }, + "node_modules/icu-minify": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.13.4.tgz", + "integrity": "sha512-yK6HyPLGlQjqm8fTKtnBpM77z7vl7JdDBN2EXLvmgAu/b7XaOHWZb73M3ISl9ahBTehBv7RYeqqWSHfk1v2YcA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5291,6 +5707,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5326,21 +5751,17 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/intl-messageformat": { - "version": "10.7.16", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", - "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==", - "license": "BSD-3-Clause", + "version": "11.2.12", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.12.tgz", + "integrity": "sha512-KW70Xxfcvy7vV3qODfvShWkFDPMqKDAa4N+hSyVBWGNtVhTUFYaqlD/l88DaYPKiVcPP4rPQ3qnH7i5K82Mg7g==", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.4", - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/icu-messageformat-parser": "2.11.2", - "tslib": "^2.8.0" + "@formatjs/fast-memoize": "3.1.7", + "@formatjs/icu-messageformat-parser": "3.5.15" } }, "node_modules/is-alphabetical": { @@ -5385,13 +5806,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true - }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -5450,7 +5864,6 @@ "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, - "license": "MIT", "dependencies": { "semver": "^7.7.1" } @@ -5469,13 +5882,12 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, - "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -5533,7 +5945,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5578,7 +5989,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5826,7 +6236,6 @@ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, - "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", @@ -5853,20 +6262,18 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" + "dev": true }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, "bin": { - "js-yaml": "bin/js-yaml.js" + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/json-buffer": { @@ -5891,16 +6298,15 @@ "license": "MIT" }, "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, "bin": { "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/jsx-ast-utils": { @@ -5920,14 +6326,13 @@ } }, "node_modules/katex": { - "version": "0.16.22", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", - "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], - "license": "MIT", "dependencies": { "commander": "^8.3.0" }, @@ -5959,41 +6364,17 @@ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", - "license": "MIT" - }, - "node_modules/langium": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", - "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", - "license": "MIT", - "dependencies": { - "chevrotain": "~11.0.3", - "chevrotain-allstar": "~0.3.0", - "vscode-languageserver": "~9.0.1", - "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.0.8" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" + "dev": true }, "node_modules/language-tags": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, - "license": "MIT", "dependencies": { "language-subtag-registry": "^0.3.20" }, @@ -6260,23 +6641,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/local-pkg": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", - "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.3.0", - "quansync": "^0.2.11" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6294,17 +6658,9 @@ } }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==" }, "node_modules/longest-streak": { "version": "3.1.0", @@ -6321,7 +6677,6 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, - "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -6343,6 +6698,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "node_modules/magic-string": { "version": "0.30.18", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", @@ -6364,10 +6734,9 @@ } }, "node_modules/marked": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.2.0.tgz", - "integrity": "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg==", - "license": "MIT", + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", "bin": { "marked": "bin/marked.js" }, @@ -6632,10 +7001,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", - "license": "MIT", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -6697,31 +7065,31 @@ } }, "node_modules/mermaid": { - "version": "11.10.1", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.10.1.tgz", - "integrity": "sha512-0PdeADVWURz7VMAX0+MiMcgfxFKY4aweSGsjgFihe3XlMKNqmai/cugMrqTd3WNHM93V+K+AZL6Wu6tB5HmxRw==", - "license": "MIT", + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", "dependencies": { - "@braintree/sanitize-url": "^7.0.4", - "@iconify/utils": "^2.1.33", - "@mermaid-js/parser": "^0.6.2", + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", - "cytoscape": "^3.29.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.11", - "dayjs": "^1.11.13", - "dompurify": "^3.2.5", - "katex": "^0.16.22", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", "khroma": "^2.1.0", - "lodash-es": "^4.17.21", - "marked": "^16.0.0", + "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "node_modules/micromark": { @@ -7321,16 +7689,18 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -7338,7 +7708,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7354,11 +7723,10 @@ } }, "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, - "license": "MIT", "dependencies": { "minipass": "^7.1.2" }, @@ -7366,51 +7734,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" - }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7418,16 +7741,15 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -7436,11 +7758,10 @@ } }, "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, - "license": "MIT", "bin": { "napi-postinstall": "lib/cli.js" }, @@ -7468,15 +7789,12 @@ } }, "node_modules/next": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz", - "integrity": "sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==", - "license": "MIT", + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.22.tgz", + "integrity": "sha512-mrtal1sRxO4YrlDS98sDuIvGZivKbFix8w7oAL9ZynfOgc3cADQOQgvwtMooc18Qr8bKzvQAcHwHZ0mbJ7zcfQ==", "dependencies": { - "@next/env": "15.3.1", - "@swc/counter": "0.1.3", + "@next/env": "15.5.22", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -7488,19 +7806,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.3.1", - "@next/swc-darwin-x64": "15.3.1", - "@next/swc-linux-arm64-gnu": "15.3.1", - "@next/swc-linux-arm64-musl": "15.3.1", - "@next/swc-linux-x64-gnu": "15.3.1", - "@next/swc-linux-x64-musl": "15.3.1", - "@next/swc-win32-arm64-msvc": "15.3.1", - "@next/swc-win32-x64-msvc": "15.3.1", - "sharp": "^0.34.1" + "@next/swc-darwin-arm64": "15.5.22", + "@next/swc-darwin-x64": "15.5.22", + "@next/swc-linux-arm64-gnu": "15.5.22", + "@next/swc-linux-arm64-musl": "15.5.22", + "@next/swc-linux-x64-gnu": "15.5.22", + "@next/swc-linux-x64-musl": "15.5.22", + "@next/swc-win32-arm64-msvc": "15.5.22", + "@next/swc-win32-x64-msvc": "15.5.22", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", @@ -7522,25 +7840,28 @@ } }, "node_modules/next-intl": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.5.tgz", - "integrity": "sha512-tT3SltfpPOCAQ9kVNr+8t6FUtVf8G0WFlJcVc8zj4WCMfuF8XFk4gZCN/MtjgDgkUISw5aKamOClJB4EsV95WQ==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.13.4.tgz", + "integrity": "sha512-jhPAT0u0lahIK6E4gVdZAehugWCosBhLG8sV7xMzgSVoJpxHObP+Fiu+z2FfkEW0XPPtr7uEXoUlLEfhxhNMTg==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/amannn" } ], - "license": "MIT", "dependencies": { - "@formatjs/intl-localematcher": "^0.5.4", + "@formatjs/intl-localematcher": "^0.8.1", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "icu-minify": "^4.13.4", "negotiator": "^1.0.0", - "use-intl": "^4.3.5" + "next-intl-swc-plugin-extractor": "^4.13.4", + "po-parser": "^2.1.1", + "use-intl": "^4.13.4" }, "peerDependencies": { - "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", - "typescript": "^5.0.0" + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -7548,6 +7869,60 @@ } } }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.13.4.tgz", + "integrity": "sha512-uN1+NMUYbG6YkO3q+rjc2bvAPX9nQ23owemvHJAyW0pRbQjVDwvNhmrV5qaak0oQc/9okbK17KLT49AoMGhVEQ==" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.46", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.46.tgz", + "integrity": "sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==", + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.27" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.46", + "@swc/core-darwin-x64": "1.15.46", + "@swc/core-linux-arm-gnueabihf": "1.15.46", + "@swc/core-linux-arm64-gnu": "1.15.46", + "@swc/core-linux-arm64-musl": "1.15.46", + "@swc/core-linux-ppc64-gnu": "1.15.46", + "@swc/core-linux-s390x-gnu": "1.15.46", + "@swc/core-linux-x64-gnu": "1.15.46", + "@swc/core-linux-x64-musl": "1.15.46", + "@swc/core-win32-arm64-msvc": "1.15.46", + "@swc/core-win32-ia32-msvc": "1.15.46", + "@swc/core-win32-x64-msvc": "1.15.46" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -7583,7 +7958,48 @@ "source-map-js": "^1.0.2" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "engines": { + "node": ">=18" } }, "node_modules/object-assign": { @@ -7591,7 +8007,6 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7645,7 +8060,6 @@ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -7680,7 +8094,6 @@ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -7778,10 +8191,9 @@ } }, "node_modules/package-manager-detector": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", - "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", - "license": "MIT" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -7866,12 +8278,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7879,11 +8285,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -7891,16 +8296,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==" }, "node_modules/points-on-curve": { "version": "0.2.0", @@ -7929,9 +8328,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "dev": true, "funding": [ { @@ -7947,9 +8346,8 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7981,7 +8379,6 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, - "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -8008,22 +8405,6 @@ "node": ">=6" } }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8079,8 +8460,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/react-markdown": { "version": "10.1.0", @@ -8110,17 +8490,19 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "15.6.3", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.3.tgz", - "integrity": "sha512-HebdyA9r20hgmA0q8RyRJ4c/vB4E6KL2HeWb5MNjU3iJEiT2w9jfU2RJsmI6f3Cy3SGE5tm0AIkBzM/E7e9/lQ==", - "license": "MIT", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", "dependencies": { - "@babel/runtime": "^7.3.1", + "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", - "refractor": "^3.6.0" + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" }, "peerDependencies": { "react": ">= 0.14.0" @@ -8150,121 +8532,20 @@ } }, "node_modules/refractor": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", - "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", - "license": "MIT", - "dependencies": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "license": "MIT", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/refractor/node_modules/prismjs": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", - "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -8438,7 +8719,6 @@ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -8455,10 +8735,9 @@ } }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==" }, "node_modules/roughjs": { "version": "4.6.6", @@ -8499,8 +8778,7 @@ "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" }, "node_modules/safe-array-concat": { "version": "1.1.3", @@ -8560,8 +8838,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/scheduler": { "version": "0.26.0", @@ -8570,11 +8847,10 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "devOptional": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -8632,16 +8908,15 @@ } }, "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, - "license": "Apache-2.0", "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -8650,28 +8925,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -8773,16 +9050,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8806,8 +9073,7 @@ "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", @@ -8823,20 +9089,11 @@ "node": ">= 0.4" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -8851,7 +9108,6 @@ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -8879,7 +9135,6 @@ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, - "license": "MIT", "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -8963,7 +9218,6 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -9028,19 +9282,6 @@ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "license": "MIT" }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -9082,17 +9323,15 @@ } }, "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, - "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { @@ -9100,20 +9339,21 @@ } }, "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "license": "MIT" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, - "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -9127,7 +9367,6 @@ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "license": "MIT", "engines": { "node": ">=12.0.0" }, @@ -9141,11 +9380,10 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -9187,11 +9425,10 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, - "license": "MIT", "engines": { "node": ">=18.12" }, @@ -9213,7 +9450,6 @@ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, - "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -9221,6 +9457,18 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -9332,11 +9580,28 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "license": "MIT" + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } }, "node_modules/unbox-primitive": { "version": "1.1.0", @@ -9480,38 +9745,70 @@ } }, "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", "dev": true, "hasInstallScript": true, - "license": "MIT", "dependencies": { - "napi-postinstall": "^0.3.0" + "napi-postinstall": "^0.3.4" }, "funding": { "url": "https://opencollective.com/unrs-resolver" }, "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, "node_modules/uri-js": { @@ -9525,30 +9822,35 @@ } }, "node_modules/use-intl": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.5.tgz", - "integrity": "sha512-qyL1TZNesVbzj/75ZbYsi+xzNSiFqp5rIVsiAN0JT8rPMSjX0/3KQz76aJIrngI1/wIQdVYFVdImWh5yAv+dWA==", - "license": "MIT", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.13.4.tgz", + "integrity": "sha512-wRhU5zyPNgu845++EJ8ckQsi89b22QUop7NlGxNXpsnKSwEJr7WErAkdAYeVQgFTmDWsa8e2NI1e14XbWz9Ecw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], "dependencies": { - "@formatjs/fast-memoize": "^2.2.0", + "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", - "intl-messageformat": "^10.5.14" + "icu-minify": "^4.13.4", + "intl-messageformat": "^11.1.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" } }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "bin": { - "uuid": "dist/esm/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/vfile": { @@ -9593,55 +9895,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", - "license": "MIT" - }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -9767,15 +10020,6 @@ "node": ">=0.10.0" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -9799,6 +10043,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 0e74c5ac9..1d84e49c3 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,14 @@ }, "dependencies": { "mermaid": "^11.4.1", - "next": "15.3.1", + "next": "^15.5.22", "next-intl": "^4.1.0", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.5.0", "react-markdown": "^10.1.0", - "react-syntax-highlighter": "^15.6.1", + "react-syntax-highlighter": "^16.1.1", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", @@ -25,14 +25,14 @@ "svg-pan-zoom": "^3.6.2" }, "devDependencies": { - "@eslint/eslintrc": "^3", + "@eslint/eslintrc": "^0.1.0", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "@types/react-syntax-highlighter": "^15.5.13", - "eslint": "^9", - "eslint-config-next": "15.3.1", + "eslint": "^10.8.0", + "eslint-config-next": "^16.2.12", "tailwindcss": "^4", "typescript": "^5" }, diff --git a/pytest.ini b/pytest.ini index 3395d3288..1b8bc661e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,7 +3,7 @@ testpaths = test python_files = test_*.py *_test.py python_classes = Test* python_functions = test_* -addopts = +addopts = -v --strict-markers --disable-warnings diff --git a/run.sh b/run.sh index c2b523807..24c82f415 100644 --- a/run.sh +++ b/run.sh @@ -1 +1 @@ -uv run -m api.main \ No newline at end of file +uv run -m api.main diff --git a/screenshots/Codemap.png b/screenshots/Codemap.png new file mode 100644 index 000000000..470d6035d Binary files /dev/null and b/screenshots/Codemap.png differ diff --git a/src/app/[owner]/[repo]/page.tsx b/src/app/[owner]/[repo]/page.tsx index 98ffc6fbc..bdedcc663 100644 --- a/src/app/[owner]/[repo]/page.tsx +++ b/src/app/[owner]/[repo]/page.tsx @@ -2,6 +2,7 @@ 'use client'; import Ask from '@/components/Ask'; +import CodeViewer, { CodeTarget } from '@/components/CodeViewer'; import Markdown from '@/components/Markdown'; import ModelSelectionModal from '@/components/ModelSelectionModal'; import ThemeToggle from '@/components/theme-toggle'; @@ -9,6 +10,7 @@ import WikiTreeView from '@/components/WikiTreeView'; import { useLanguage } from '@/contexts/LanguageContext'; import { RepoInfo } from '@/types/repoinfo'; import getRepoUrl from '@/utils/getRepoUrl'; +import { prepareRepoIndex } from '@/utils/prepareRepo'; import { extractUrlDomain, extractUrlPath } from '@/utils/urlDecoder'; import Link from 'next/link'; import { useParams, useSearchParams } from 'next/navigation'; @@ -275,6 +277,17 @@ export default function RepoWikiPage() { const [isAskModalOpen, setIsAskModalOpen] = useState(false); const askComponentRef = useRef<{ clearConversation: () => void } | null>(null); + // Code viewer drawer (rendered at modal level so its header isn't clipped by + // the Ask panel's scroll container). Opened by codemap citation clicks. + const [codeViewerOpen, setCodeViewerOpen] = useState(false); + const [codeViewerTarget, setCodeViewerTarget] = useState(null); + const [codeViewerFiles, setCodeViewerFiles] = useState([]); + const openCodeViewer = useCallback((target: CodeTarget, files: string[]) => { + setCodeViewerFiles(files); + setCodeViewerTarget(target); + setCodeViewerOpen(true); + }, []); + // Authentication state const [authRequired, setAuthRequired] = useState(false); const [authCode, setAuthCode] = useState(''); @@ -298,7 +311,7 @@ export default function RepoWikiPage() { try { const url = new URL(repoUrl); const hostname = url.hostname; - + if (hostname === 'github.com' || hostname.includes('github')) { // GitHub URL format: https://github.com/owner/repo/blob/branch/path return `${repoUrl}/blob/${defaultBranch}/${filePath}`; @@ -317,6 +330,150 @@ export default function RepoWikiPage() { return filePath; }, [effectiveRepoInfo, defaultBranch]); + // Post-process the LLM-generated wiki markdown to fix two recurring format + // issues that the prompt alone cannot reliably prevent: + // 1. Normalize the leading "Relevant source files"
block so it + // always uses the exact, program-generated markdown. The model often + // drops the links (rendering plain text) or appends bogus line numbers. + // 2. Resolve empty citation links `[file.ext:10-20]()` to real repository + // URLs so they render as clickable links instead of dead ones. + const postProcessWikiContent = useCallback((content: string, filePaths: string[]): string => { + let processed = content; + + // Escape the characters that would otherwise break a Markdown link label. + // File paths such as Next.js dynamic routes (src/app/[owner]/[repo]/page.tsx) + // contain '[' / ']' and MUST be escaped, or Markdown parses them as nested + // links and the citation renders as garbage. + const escapeLabel = (s: string) => s.replace(/([[\]])/g, '\\$1'); + + // Build the host-specific line anchor for an already-resolved file URL. + // GitHub: #L10-L20 (single: #L10) + // GitLab: #L10-20 (single: #L10) + // Bitbucket: #lines-10:20 (single: #lines-10) + // Detect the host from the hostname only (mirroring generateFileUrl) so a + // repo/owner name that happens to contain another vendor's name in the URL + // path cannot cause a misclassification. + const lineAnchor = (url: string, start: string, end?: string): string => { + let hostname = ''; + try { + hostname = new URL(url).hostname; + } catch { + hostname = ''; + } + if (hostname.includes('github')) return end ? `#L${start}-L${end}` : `#L${start}`; + if (hostname.includes('gitlab')) return end ? `#L${start}-${end}` : `#L${start}`; + if (hostname.includes('bitbucket')) return end ? `#lines-${start}:${end}` : `#lines-${start}`; + return ''; + }; + + // 1. Rebuild the
block from the known file list. + if (filePaths.length > 0) { + const detailsBlock = `
+Relevant source files + +The following files were used as context for generating this wiki page: + +${filePaths.map(path => `- [${escapeLabel(path)}](${generateFileUrl(path)})`).join('\n')} +
`; + + const detailsRegex = /
\s*\s*Relevant source files\s*<\/summary>[\s\S]*?<\/details>/i; + if (detailsRegex.test(processed)) { + // Replace whatever the model produced, in place, with the canonical block. + processed = processed.replace(detailsRegex, detailsBlock); + } else { + // The model omitted the block entirely; prepend the canonical one. + processed = `${detailsBlock}\n\n${processed}`; + } + } + + // 2. Resolve empty citation links `[path/to/file.ext:10-20]()` -> real URL. + // Match ONLY against the known source-file paths (longest first, so the + // most specific path wins). This is far safer than a generic `[...]()` + // regex: malformed or nested brackets in the model output can no longer + // be swallowed into a bogus label/URL, and paths containing '[' / ']' + // are matched literally instead of tripping the Markdown parser. + if (filePaths.length > 0) { + const alternation = [...filePaths] + .sort((a, b) => b.length - a.length) + .map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + .join('|'); + const citationRegex = new RegExp(`\\[(${alternation})(?::(\\d+)(?:-(\\d+))?)?\\]\\(\\)`, 'g'); + processed = processed.replace(citationRegex, (match, path: string, start: string, end: string) => { + const url = generateFileUrl(path); + // generateFileUrl returns the bare path for local repos / unresolved + // hosts; in that case there is no web URL to link to, so leave it as-is. + if (url === path) { + return match; + } + const linePart = start ? (end ? `:${start}-${end}` : `:${start}`) : ''; + const anchor = start ? lineAnchor(url, start, end) : ''; + return `[${escapeLabel(path)}${linePart}](${url}${anchor})`; + }); + } + + // 3. Resolve any REMAINING empty citation links that look like a repository + // file path but were not in this page's filePaths. The model frequently + // cites additional files it read (e.g. accelerator_connector.py) that were + // never in the assigned list, leaving them as dead `[path:10-20]()` links. + // The path token forbids brackets/whitespace/parens, so nested-bracket + // edge cases are still avoided; bracketed paths (e.g. Next.js dynamic + // routes) are already handled by the filePaths pass above. + const genericCitationRegex = + /\[([^[\]\s()]+?\.[A-Za-z0-9]+)(?::(\d+)(?:-(\d+))?)?\]\(\)/g; + processed = processed.replace( + genericCitationRegex, + (match, path: string, start: string, end: string) => { + const url = generateFileUrl(path); + if (url === path) { + return match; // local repo / unresolved host -> leave as-is + } + const linePart = start ? (end ? `:${start}-${end}` : `:${start}`) : ''; + const anchor = start ? lineAnchor(url, start, end) : ''; + return `[${escapeLabel(path)}${linePart}](${url}${anchor})`; + }, + ); + + // 4. Resolve citations where the model put a "Sources:" prefix INSIDE the + // bracket and/or used a bare filename instead of the full repo path, e.g. + // `[Sources: fit_loop.py:56-104]()`. The bare name is mapped back to a + // full path via this page's filePaths (by basename); the "Sources:" label + // is moved outside the link to match the normal `Sources: [file](url)` + // format. Unknown bare names are left untouched. + if (filePaths.length > 0) { + const byBasename = new Map(); + for (const p of filePaths) { + const base = p.split('/').pop() ?? p; + if (!byBasename.has(base)) byBasename.set(base, p); + } + const prefixedCitationRegex = + /\[(Sources?|Source):\s*([^[\]\s():]+?)(?::(\d+)(?:-(\d+))?)?\]\(\)/gi; + processed = processed.replace( + prefixedCitationRegex, + (match, prefix: string, token: string, start: string, end: string) => { + // token may already be a full path, or a bare basename to look up. + const fullPath = token.includes('/') ? token : byBasename.get(token); + if (!fullPath) { + return match; // unknown bare filename -> leave as-is + } + const url = generateFileUrl(fullPath); + if (url === fullPath) { + return match; // local repo / unresolved host + } + const linePart = start ? (end ? `:${start}-${end}` : `:${start}`) : ''; + const anchor = start ? lineAnchor(url, start, end) : ''; + return `${prefix}: [${escapeLabel(fullPath)}${linePart}](${url}${anchor})`; + }, + ); + } + + // 5. Strip a redundant empty "()" left immediately after a completed link. + // The model sometimes emits `[path](https://…)()` — a real link followed + // by the citation template's empty parens — which renders a stray "()". + processed = processed.replace(/(\]\([^)\s]+\))\(\)/g, '$1'); + + return processed; + }, [generateFileUrl]); + // Memoize repo info to avoid triggering updates in callbacks // Add useEffect to handle scroll reset @@ -426,11 +583,11 @@ You will be given: CRITICAL STARTING INSTRUCTION: The very first thing on the page MUST be a \`
\` block listing ALL the \`[RELEVANT_SOURCE_FILES]\` you used to generate the content. There MUST be AT LEAST 5 source files listed - if fewer were provided, you MUST find additional related files to include. -Format it exactly like this: +Do not provide any acknowledgements, disclaimers, apologies, or any other preface before the \`
\` block. JUST START with the \`
\` block. +Format the block EXACTLY like the following template, reproducing it verbatim (do not add line numbers, do not convert the links to plain text, do not add any other text):
Relevant source files -Remember, do not provide any acknowledgements, disclaimers, apologies, or any other preface before the \`
\` block. JUST START with the \`
\` block. The following files were used as context for generating this wiki page: ${filePaths.map(path => `- [${path}](${generateFileUrl(path)})`).join('\n')} @@ -497,7 +654,12 @@ Based ONLY on the content of the \`[RELEVANT_SOURCE_FILES]\`: 6. **Source Citations (EXTREMELY IMPORTANT):** * For EVERY piece of significant information, explanation, diagram, table entry, or code snippet, you MUST cite the specific source file(s) and relevant line numbers from which the information was derived. * Place citations at the end of the paragraph, under the diagram/table, or after the code snippet. - * Use the exact format: \`Sources: [filename.ext:start_line-end_line]()\` for a range, or \`Sources: [filename.ext:line_number]()\` for a single line. Multiple files can be cited: \`Sources: [file1.ext:1-10](), [file2.ext:5](), [dir/file3.ext]()\` (if the whole file is relevant and line numbers are not applicable or too broad). + * Use the EXACT format below, and ALWAYS use the FULL repository-relative path exactly as it appears in the "Relevant source files" list above — NEVER a bare filename (e.g. use \`src/lightning/pytorch/loops/fit_loop.py\`, not \`fit_loop.py\`): + * Range: \`Sources: [src/full/path/file.ext:start_line-end_line]()\` + * Single line: \`Sources: [src/full/path/file.ext:line_number]()\` + * Multiple files: \`Sources: [src/full/path/a.ext:1-10](), [src/full/path/b.ext:5](), [src/full/path/c.ext]()\` (omit line numbers when the whole file is relevant). + * The word \`Sources:\` MUST be placed BEFORE the opening bracket, never inside it (write \`Sources: [path]()\`, NOT \`[Sources: path]()\`). + * Leave the parentheses \`()\` EMPTY — they are resolved into real links automatically. Do not put a URL inside them. * If an entire section is overwhelmingly based on one or two files, you can cite them under the section heading in addition to more specific citations within the section. * IMPORTANT: You MUST cite AT LEAST 5 different source files throughout the wiki page to ensure comprehensive coverage. @@ -513,7 +675,7 @@ IMPORTANT: Generate the content in ${language === 'en' ? 'English' : language === 'zh-tw' ? 'Traditional Chinese (繁體中文)' : language === 'es' ? 'Spanish (Español)' : language === 'kr' ? 'Korean (한국어)' : - language === 'vi' ? 'Vietnamese (Tiếng Việt)' : + language === 'vi' ? 'Vietnamese (Tiếng Việt)' : language === "pt-br" ? "Brazilian Portuguese (Português Brasileiro)" : language === "fr" ? "Français (French)" : language === "ru" ? "Русский (Russian)" : @@ -644,6 +806,9 @@ Remember: // Clean up markdown delimiters content = content.replace(/^```markdown\s*/i, '').replace(/```\s*$/i, ''); + // Normalize the
block and resolve empty citation links. + content = postProcessWikiContent(content, filePaths); + console.log(`Received content for ${page.title}, length: ${content.length} characters`); // Store the FINAL generated content @@ -678,7 +843,7 @@ Remember: setLoadingMessage(undefined); // Clear specific loading message } }); - }, [generatedPages, currentToken, effectiveRepoInfo, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, modelExcludedDirs, modelExcludedFiles, language, activeContentRequests, generateFileUrl]); + }, [generatedPages, currentToken, effectiveRepoInfo, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, modelExcludedDirs, modelExcludedFiles, language, activeContentRequests, generateFileUrl, postProcessWikiContent]); // Determine the wiki structure from repository data const determineWikiStructure = useCallback(async (fileTree: string, readme: string, owner: string, repo: string) => { @@ -950,6 +1115,11 @@ IMPORTANT: let xmlText = xmlMatch[0]; xmlText = xmlText.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); + // Escape bare ampersands that are not part of a valid XML entity. A single + // unescaped '&' (very common in LLM-generated titles/descriptions such as + // "Frontend & Backend") makes strict text/xml parsing fail with a + // , which would otherwise drop the whole structure. + xmlText = xmlText.replace(/&(?!(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);)/g, '&'); // Try parsing with DOMParser const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); @@ -983,10 +1153,6 @@ IMPORTANT: // Parse pages using DOM pages = []; - if (parseError && (!pagesEls || pagesEls.length === 0)) { - console.warn('DOM parsing failed, trying regex fallback'); - } - pagesEls.forEach(pageEl => { const id = pageEl.getAttribute('id') || `page-${pages.length + 1}`; const titleEl = pageEl.querySelector('title'); @@ -1019,6 +1185,28 @@ IMPORTANT: }); }); + // Regex fallback: strict text/xml parsing can still fail (or yield no + // nodes) on malformed LLM output. Recover pages directly from the + // raw XML text so a parse hiccup does not produce an empty wiki. + if (pages.length === 0) { + console.warn('DOM parsing yielded no pages; using regex fallback'); + const pageBlocks = xmlText.match(//g) || []; + pages = pageBlocks.map((block, i) => { + const pid = block.match(/([\s\S]*?)<\/title>/)?.[1]?.trim() ?? ''; + const imp = block.match(/([\s\S]*?)<\/importance>/)?.[1]?.trim(); + const importance: 'high' | 'medium' | 'low' = + imp === 'high' ? 'high' : imp === 'low' ? 'low' : 'medium'; + const filePaths = Array.from( + block.matchAll(/([\s\S]*?)<\/file_path>/g), + ).map(m => m[1].trim()).filter(Boolean); + const relatedPages = Array.from( + block.matchAll(/([\s\S]*?)<\/related>/g), + ).map(m => m[1].trim()).filter(Boolean); + return { id: pid, title: ptitle, content: '', filePaths, importance, relatedPages }; + }); + } + // Extract sections if they exist in the XML const sections: WikiSection[] = []; const rootSections: string[] = []; @@ -1218,16 +1406,16 @@ IMPORTANT: if (!repoUrl) { return 'https://api.github.com'; // Default to public GitHub } - + try { const url = new URL(repoUrl); const hostname = url.hostname; - + // If it's the public GitHub, use the standard API URL if (hostname === 'github.com') { return 'https://api.github.com'; } - + // For GitHub Enterprise, use the enterprise API URL format // GitHub Enterprise API URL format: https://github.company.com/api/v3 return `${url.protocol}//${hostname}/api/v3`; @@ -1243,7 +1431,7 @@ IMPORTANT: const repoInfoResponse = await fetch(`${githubApiBaseUrl}/repos/${owner}/${repo}`, { headers: createGithubHeaders(currentToken) }); - + if (repoInfoResponse.ok) { const repoData = await repoInfoResponse.json(); defaultBranchLocal = repoData.default_branch; @@ -1256,7 +1444,7 @@ IMPORTANT: } // Create list of branches to try, prioritizing the actual default branch - const branchesToTry = defaultBranchLocal + const branchesToTry = defaultBranchLocal ? [defaultBranchLocal, 'main', 'master'].filter((branch, index, arr) => arr.indexOf(branch) === index) : ['main', 'master']; @@ -1353,7 +1541,7 @@ IMPORTANT: // Step 2: Paginate to fetch full file tree let page = 1; let morePages = true; - + while (morePages) { const apiUrl = `${projectInfoUrl}/repository/tree?recursive=true&per_page=100&page=${page}`; const response = await fetch(apiUrl, { headers }); @@ -1480,6 +1668,40 @@ IMPORTANT: } } + // Warm the backend embedding index BEFORE the first chat call. This moves + // the slow, one-time cold embedding to a dedicated streaming endpoint (with + // progress) so determineWikiStructure hits a warm cache instead of blocking + // long enough to trigger a proxy headers timeout. No-op if already indexed. + const preparingIndexMsg = messages.loading?.preparingIndex || 'Preparing repository index...'; + try { + const prepareBody: Record = { + repo_url: getRepoUrl(effectiveRepoInfo), + type: effectiveRepoInfo.type, + }; + addTokensToRequestBody( + prepareBody, + currentToken, + effectiveRepoInfo.type, + selectedProviderState, + selectedModelState, + isCustomSelectedModelState, + customSelectedModelState, + language, + modelExcludedDirs, + modelExcludedFiles, + modelIncludedDirs, + modelIncludedFiles, + ); + setLoadingMessage(preparingIndexMsg); + await prepareRepoIndex(prepareBody, ({ elapsedSec }) => { + setLoadingMessage(elapsedSec ? `${preparingIndexMsg} (${elapsedSec}s)` : preparingIndexMsg); + }); + } catch (prepareError) { + // Non-fatal: determineWikiStructure will build the index on demand as a + // fallback (slower). Log and continue so a prepare hiccup never blocks wiki generation. + console.warn('Repository index prepare failed; continuing with on-demand build:', prepareError); + } + // Now determine the wiki structure await determineWikiStructure(fileTreeData, readmeContent, owner, repo); @@ -1492,7 +1714,7 @@ IMPORTANT: // Reset the request in progress flag setRequestInProgress(false); } - }, [owner, repo, determineWikiStructure, currentToken, effectiveRepoInfo, requestInProgress, messages.loading]); + }, [owner, repo, determineWikiStructure, currentToken, effectiveRepoInfo, requestInProgress, messages.loading, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, language, modelExcludedDirs, modelExcludedFiles, modelIncludedDirs, modelIncludedFiles]); // Function to export wiki content const exportWiki = useCallback(async (format: 'markdown' | 'json') => { @@ -1844,7 +2066,7 @@ IMPORTANT: setGeneratedPages(cachedData.generated_pages); setCurrentPageId(cachedStructure.pages.length > 0 ? cachedStructure.pages[0].id : undefined); setIsLoading(false); - setEmbeddingError(false); + setEmbeddingError(false); setLoadingMessage(undefined); cacheLoadedSuccessfully.current = true; return; // Exit if cache is successfully loaded @@ -2207,7 +2429,7 @@ IMPORTANT: {/* Floating Chat Button */} {!isLoading && wikiStructure && ( -
- (askComponentRef.current = ref)} - /> + + {/* Split layout: conversation on the left, code viewer on the right. + When no codemap citation needs a viewer, the conversation uses the + full width (same as fast / deep-research modes). ~6:4 when open. */} +
+
+ (askComponentRef.current = ref)} + onOpenCodeViewer={openCodeViewer} + onCloseCodeViewer={() => setCodeViewerOpen(false)} + /> +
+ {codeViewerOpen && ( +
+ + setCodeViewerTarget({ file_path: f, start_line: null, end_line: null, snippet: '' }) + } + /> +
+ )}
diff --git a/src/app/[owner]/[repo]/workshop/page.tsx b/src/app/[owner]/[repo]/workshop/page.tsx index 7a1e29c75..83abe29c8 100644 --- a/src/app/[owner]/[repo]/workshop/page.tsx +++ b/src/app/[owner]/[repo]/workshop/page.tsx @@ -301,7 +301,7 @@ Make the workshop content in ${language === 'en' ? 'English' : language === 'zh-tw' ? 'Traditional Chinese (繁體中文)' : language === 'es' ? 'Spanish (Español)' : language === 'kr' ? 'Korean (한국어)' : - language === 'vi' ? 'Vietnamese (Tiếng Việt)' : + language === 'vi' ? 'Vietnamese (Tiếng Việt)' : language === "pt-br" ? "Brazilian Portuguese (Português Brasileiro)" : language === "fr" ? "Français (French)" : language === "ru" ? "Русский (Russian)" : diff --git a/src/app/api/auth/status/route.ts b/src/app/api/auth/status/route.ts index 601bb30b2..5bb7c6666 100644 --- a/src/app/api/auth/status/route.ts +++ b/src/app/api/auth/status/route.ts @@ -11,14 +11,14 @@ export async function GET() { 'Content-Type': 'application/json', }, }); - + if (!response.ok) { return NextResponse.json( { error: `Backend server returned ${response.status}` }, { status: response.status } ); } - + const data = await response.json(); return NextResponse.json(data); } catch (error) { diff --git a/src/app/api/auth/validate/route.ts b/src/app/api/auth/validate/route.ts index 0a9b7c0a8..8c82c928b 100644 --- a/src/app/api/auth/validate/route.ts +++ b/src/app/api/auth/validate/route.ts @@ -5,7 +5,7 @@ const TARGET_SERVER_BASE_URL = process.env.SERVER_BASE_URL || 'http://localhost: export async function POST(request: NextRequest) { try { const body = await request.json(); - + // Forward the request to the backend API const response = await fetch(`${TARGET_SERVER_BASE_URL}/auth/validate`, { method: 'POST', @@ -14,14 +14,14 @@ export async function POST(request: NextRequest) { }, body: JSON.stringify(body), }); - + if (!response.ok) { return NextResponse.json( { error: `Backend server returned ${response.status}` }, { status: response.status } ); } - + const data = await response.json(); return NextResponse.json(data); } catch (error) { diff --git a/src/app/api/chat/stream/route.ts b/src/app/api/chat/stream/route.ts index 544c5d1eb..9d3afb3ae 100644 --- a/src/app/api/chat/stream/route.ts +++ b/src/app/api/chat/stream/route.ts @@ -110,4 +110,4 @@ export async function OPTIONS() { 'Access-Control-Allow-Headers': 'Content-Type, Authorization', // Adjust as per client's request headers }, }); -} \ No newline at end of file +} diff --git a/src/app/api/models/config/route.ts b/src/app/api/models/config/route.ts index 802df2217..01a57c793 100644 --- a/src/app/api/models/config/route.ts +++ b/src/app/api/models/config/route.ts @@ -27,7 +27,7 @@ export async function GET() { const modelConfig = await backendResponse.json(); return NextResponse.json(modelConfig); } catch (error) { - console.error('Error fetching model configurations:', error); + console.error('Error fetching model configurations:', error); return new NextResponse(JSON.stringify({ error: error }), { status: 500, headers: { 'Content-Type': 'application/json' }, diff --git a/src/app/api/repo/prepare/route.ts b/src/app/api/repo/prepare/route.ts new file mode 100644 index 000000000..42ad43f89 --- /dev/null +++ b/src/app/api/repo/prepare/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server'; + +// Proxy for the backend's repository-indexing endpoint. +// +// The backend streams Server-Sent Events (a heartbeat/progress line at least +// every ~10s), so this route just pipes the byte stream straight through. No +// custom fetch timeout is needed: the first byte (headers) arrives immediately +// and heartbeats keep the body well within undici's default 300s timeouts. +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +const TARGET_SERVER_BASE_URL = process.env.SERVER_BASE_URL || 'http://localhost:8001'; + +export async function POST(req: NextRequest) { + try { + const requestBody = await req.json(); + const targetUrl = `${TARGET_SERVER_BASE_URL}/repo/prepare`; + + const backendResponse = await fetch(targetUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + }, + body: JSON.stringify(requestBody), + }); + + if (!backendResponse.ok || !backendResponse.body) { + const errorBody = await backendResponse.text().catch(() => ''); + return new NextResponse(errorBody || 'Failed to start repository indexing', { + status: backendResponse.status || 500, + }); + } + + // Pipe the SSE stream from the backend to the client. + const stream = new ReadableStream({ + async start(controller) { + const reader = backendResponse.body!.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + controller.enqueue(value); + } + } catch (error) { + console.error('Error reading from backend prepare stream in proxy:', error); + controller.error(error); + } finally { + controller.close(); + reader.releaseLock(); + } + }, + cancel(reason) { + console.log('Client cancelled repo prepare stream:', reason); + }, + }); + + return new NextResponse(stream, { + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + 'X-Accel-Buffering': 'no', + }, + }); + } catch (error) { + console.error('Error in API proxy route (/api/repo/prepare):', error); + const errorMessage = error instanceof Error ? error.message : 'Internal Server Error in proxy'; + return new NextResponse(JSON.stringify({ error: errorMessage }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} diff --git a/src/app/api/wiki/projects/route.ts b/src/app/api/wiki/projects/route.ts index f87ed35d9..1c2d3d948 100644 --- a/src/app/api/wiki/projects/route.ts +++ b/src/app/api/wiki/projects/route.ts @@ -101,4 +101,4 @@ export async function DELETE(request: Request) { const message = error instanceof Error ? error.message : 'An unknown error occurred'; return NextResponse.json({ error: `Failed to delete project: ${message}` }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 9e05a2ef9..5be90c925 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -622,4 +622,4 @@ export default function Home() { ); -} \ No newline at end of file +} diff --git a/src/app/wiki/projects/page.tsx b/src/app/wiki/projects/page.tsx index 751286c8d..75522ecba 100644 --- a/src/app/wiki/projects/page.tsx +++ b/src/app/wiki/projects/page.tsx @@ -16,4 +16,4 @@ export default function WikiProjectsPage() { /> ); -} \ No newline at end of file +} diff --git a/src/components/Ask.tsx b/src/components/Ask.tsx index e91f0b066..3f09de38d 100644 --- a/src/components/Ask.tsx +++ b/src/components/Ask.tsx @@ -1,13 +1,44 @@ 'use client'; import React, {useState, useRef, useEffect} from 'react'; -import {FaChevronLeft, FaChevronRight } from 'react-icons/fa'; +import { FaChevronLeft, FaChevronRight, FaBolt, FaMicroscope, FaBook, FaChevronDown, FaCheck } from 'react-icons/fa'; import Markdown from './Markdown'; +import CodeMap, { PhaseStatus } from './CodeMap'; +import type { CodeTarget } from './CodeViewer'; import { useLanguage } from '@/contexts/LanguageContext'; import RepoInfo from '@/types/repoinfo'; import getRepoUrl from '@/utils/getRepoUrl'; import ModelSelectionModal from './ModelSelectionModal'; -import { createChatWebSocket, closeWebSocket, ChatCompletionRequest } from '@/utils/websocketClient'; +import { + createChatWebSocket, + closeWebSocket, + ChatCompletionRequest, + createCodemapWebSocket, + CodemapRequest, + CodemapData, + CodemapCitation, + CodemapPhase, +} from '@/utils/websocketClient'; + +const DONE_PHASES: Record = { + analyzing: 'done', + initial_codemap: 'done', + diagrams: 'done', +}; +const IDLE_PHASES: Record = { + analyzing: 'pending', + initial_codemap: 'pending', + diagrams: 'pending', +}; + +// Chat mode selector options (replaces the standalone Deep Research / Codemap toggles). +type ChatMode = 'fast' | 'deep_research' | 'codemap'; + +const MODES: { id: ChatMode; label: string; description: string; icon: React.ReactNode }[] = [ + { id: 'fast', label: 'Fast', description: 'Quick and responsive answers', icon: }, + { id: 'deep_research', label: 'Deep Research', description: 'More thorough investigation', icon: }, + { id: 'codemap', label: 'Codemap', description: 'Structured explanation grounded in code', icon: }, +]; interface Model { id: string; @@ -34,6 +65,32 @@ interface ResearchStage { type: 'plan' | 'update' | 'conclusion'; } +// A finished exchange kept in the conversation log so previous +// question/answer pairs are preserved instead of being overwritten. +// A committed chat (normal / deep-research) exchange. +interface ChatTurn { + kind: 'chat'; + question: string; + response: string; + mode: 'normal' | 'deep_research'; + // For deep research turns, every iteration/stage is preserved so the + // committed section can still browse the full multi-turn investigation. + researchStages?: ResearchStage[]; + // Index of the stage currently shown for this turn. + stageIndex?: number; +} + +// A committed codemap exchange. +interface CodemapTurn { + kind: 'codemap'; + question: string; + data: CodemapData; +} + +// Unified, ordered conversation entry — chat and codemap turns share one list +// so they render in the true chronological order they were asked. +type ConversationTurn = ChatTurn | CodemapTurn; + interface AskProps { repoInfo: RepoInfo; provider?: string; @@ -42,6 +99,12 @@ interface AskProps { customModel?: string; language?: string; onRef?: (ref: { clearConversation: () => void }) => void; + // Notifies the parent to open the right-side code viewer (rendered at the + // modal level so it isn't clipped by this component's scroll container). + onOpenCodeViewer?: (target: CodeTarget, files: string[]) => void; + // Notifies the parent to close the code viewer (e.g. non-codemap query, + // codemap with no citations, or conversation cleared). + onCloseCodeViewer?: () => void; } const Ask: React.FC = ({ @@ -51,13 +114,25 @@ const Ask: React.FC = ({ isCustomModel = false, customModel = '', language = 'en', - onRef + onRef, + onOpenCodeViewer, + onCloseCodeViewer, }) => { const [question, setQuestion] = useState(''); const [response, setResponse] = useState(''); const [isLoading, setIsLoading] = useState(false); const [deepResearch, setDeepResearch] = useState(false); + // Codemap state (a parallel flow to the normal/deep-research chat). + const [codemapMode, setCodemapMode] = useState(false); + const [modeMenuOpen, setModeMenuOpen] = useState(false); + const [codemapActive, setCodemapActive] = useState(false); // a codemap is generating + const [codemapQuestion, setCodemapQuestion] = useState(''); + const [codemapData, setCodemapData] = useState(null); + const [codemapError, setCodemapError] = useState(null); + const [codemapPhaseStatus, setCodemapPhaseStatus] = + useState>(IDLE_PHASES); + // Model selection state const [selectedProvider, setSelectedProvider] = useState(provider); const [selectedModel, setSelectedModel] = useState(model); @@ -75,8 +150,13 @@ const Ask: React.FC = ({ const [conversationHistory, setConversationHistory] = useState([]); const [researchIteration, setResearchIteration] = useState(0); const [researchComplete, setResearchComplete] = useState(false); + // Completed exchanges rendered as stacked sections above the live response. + const [conversationTurns, setConversationTurns] = useState([]); + // The question that produced the currently-streaming response. + const [currentQuestion, setCurrentQuestion] = useState(''); const inputRef = useRef(null); const responseRef = useRef(null); + const conversationEndRef = useRef(null); const providerRef = useRef(provider); const modelRef = useRef(model); @@ -94,12 +174,15 @@ const Ask: React.FC = ({ } }, [onRef]); - // Scroll to bottom of response when it changes + // Keep the newest content in view as the conversation grows or streams. useEffect(() => { if (responseRef.current) { responseRef.current.scrollTop = responseRef.current.scrollHeight; } - }, [response]); + if (conversationEndRef.current) { + conversationEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' }); + } + }, [response, conversationTurns]); // Close WebSocket when component unmounts useEffect(() => { @@ -153,16 +236,40 @@ const Ask: React.FC = ({ setQuestion(''); setResponse(''); setConversationHistory([]); + setConversationTurns([]); + setCurrentQuestion(''); setResearchIteration(0); setResearchComplete(false); setResearchStages([]); setCurrentStageIndex(0); + // Codemap resets + setCodemapActive(false); + setCodemapQuestion(''); + setCodemapData(null); + setCodemapError(null); + setCodemapPhaseStatus(IDLE_PHASES); + onCloseCodeViewer?.(); if (inputRef.current) { inputRef.current.focus(); } }; - const downloadresponse = () =>{ - const blob = new Blob([response], { type: 'text/markdown' }); + + // Build the message list for previously completed turns so follow-up + // questions carry the earlier conversation as context. + const buildHistoryFromTurns = (): Message[] => + conversationTurns.flatMap(turn => + turn.kind === 'codemap' + ? [ + { role: 'user' as const, content: turn.question, mode: 'normal' as const }, + { role: 'assistant' as const, content: turn.data.summary || turn.data.title }, + ] + : [ + { role: 'user' as const, content: turn.question, mode: turn.mode }, + { role: 'assistant' as const, content: turn.response }, + ] + ); + const downloadresponse = (content: string = response) =>{ + const blob = new Blob([content], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -277,6 +384,9 @@ const Ask: React.FC = ({ // WebSocket reference const webSocketRef = useRef(null); + // True while an HTTP fallback is running, so the WebSocket close handler + // doesn't also commit a (partial/errored) turn. + const fallbackActiveRef = useRef(false); // Function to continue research automatically const continueResearch = async () => { @@ -378,10 +488,12 @@ const Ask: React.FC = ({ setResponse(prev => prev + '\n\nError: WebSocket connection failed. Falling back to HTTP...'); // Fallback to HTTP if WebSocket fails - fallbackToHttp(requestBody); + fallbackActiveRef.current = true; + fallbackToHttp(requestBody, currentQuestion, 'deep_research'); }, // Close handler () => { + if (fallbackActiveRef.current) return; // Check if research is complete when the WebSocket closes const isComplete = checkIfResearchComplete(fullResponse); @@ -410,7 +522,11 @@ const Ask: React.FC = ({ }; // Fallback to HTTP if WebSocket fails - const fallbackToHttp = async (requestBody: ChatCompletionRequest) => { + const fallbackToHttp = async ( + requestBody: ChatCompletionRequest, + committedQuestion: string, + mode: 'normal' | 'deep_research' + ) => { try { // Make the API call using HTTP const apiResponse = await fetch(`/api/chat/stream`, { @@ -445,7 +561,7 @@ const Ask: React.FC = ({ // Extract research stage if this is a deep research response if (deepResearch) { - const stage = extractResearchStage(fullResponse, researchIteration); + const stage = extractResearchStage(fullResponse, requestBody.research_iteration ?? 1); if (stage) { // Add the stage to the research stages setResearchStages(prev => { @@ -462,26 +578,40 @@ const Ask: React.FC = ({ } } - // Check if research is complete - const isComplete = checkIfResearchComplete(fullResponse); - - // Force completion after a maximum number of iterations (5) - const forceComplete = researchIteration >= 5; - - if (forceComplete && !isComplete) { - // If we're forcing completion, append a comprehensive conclusion to the response - const completionNote = "\n\n## Final Conclusion\nAfter multiple iterations of deep research, we've gathered significant insights about this topic. This concludes our investigation process, having reached the maximum number of research iterations. The findings presented across all iterations collectively form our comprehensive answer to the original question."; - fullResponse += completionNote; - setResponse(fullResponse); - setResearchComplete(true); + if (mode === 'normal') { + // Normal mode: append the finished answer to the conversation log. + commitTurn(committedQuestion, fullResponse, 'normal'); } else { - setResearchComplete(isComplete); + // Deep research: mirror the WebSocket close handlers so the iteration + // counter advances and the auto-continue loop keeps running even when + // every request falls back to HTTP (the WebSocket never connects). + const iter = requestBody.research_iteration ?? 1; + if (iter === 1) { + // Iteration 1 is the research plan — always advance into the + // subsequent iterations. Advancing here (after the stream finishes) + // means the next turn captures this real answer, not a stale note. + setResearchComplete(false); + setResearchIteration(1); + } else if (iter >= 5) { + // Force a conclusion once the maximum iteration count is reached. + const isComplete = checkIfResearchComplete(fullResponse); + if (!isComplete) { + const completionNote = "\n\n## Final Conclusion\nAfter multiple iterations of deep research, we've gathered significant insights about this topic. This concludes our investigation process, having reached the maximum number of research iterations. The findings presented across all iterations collectively form our comprehensive answer to the original question."; + fullResponse += completionNote; + setResponse(fullResponse); + } + setResearchComplete(true); + } else { + // Intermediate iterations: stop only if the model signalled completion. + setResearchComplete(checkIfResearchComplete(fullResponse)); + } } } catch (error) { console.error('Error during HTTP fallback:', error); setResponse(prev => prev + '\n\nError: Failed to get a response. Please try again.'); setResearchComplete(true); } finally { + fallbackActiveRef.current = false; setIsLoading(false); } }; @@ -489,7 +619,9 @@ const Ask: React.FC = ({ // Effect to continue research when response is updated useEffect(() => { if (deepResearch && response && !isLoading && !researchComplete) { - const isComplete = checkIfResearchComplete(response); + // Iteration 1 is the plan and is never "complete"; only apply the + // completion heuristic from the second iteration onward. + const isComplete = researchIteration >= 2 && checkIfResearchComplete(response); if (isComplete) { setResearchComplete(true); } else if (researchIteration > 0 && researchIteration < 5) { @@ -504,6 +636,18 @@ const Ask: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [response, isLoading, deepResearch, researchComplete, researchIteration]); + // When a deep research process finishes, move the final answer into the + // conversation log so it is preserved as its own section. + useEffect(() => { + if (deepResearch && researchComplete && !isLoading && response && currentQuestion) { + commitTurn(currentQuestion, response, 'deep_research', researchStages); + setResearchIteration(0); + setResearchComplete(false); + fallbackActiveRef.current = false; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [researchComplete, isLoading, researchStages]); + // Effect to update research stages when the response changes useEffect(() => { if (deepResearch && response && !isLoading) { @@ -541,26 +685,152 @@ const Ask: React.FC = ({ if (!question.trim() || isLoading) return; + if (codemapMode) { + handleCodemapAsk(); + return; + } handleConfirmAsk(); }; + // Unique set of files cited across a codemap (used as viewer tabs). + const collectCodemapFiles = (data: CodemapData): string[] => { + const files = new Set(); + data.sections.forEach((s) => + s.steps.forEach((st) => { + if (st.citation?.file_path) files.add(st.citation.file_path); + }) + ); + return Array.from(files); + }; + + // First step citation across all sections (used to auto-open the viewer). + const firstCitation = (data: CodemapData): CodemapCitation | null => { + for (const section of data.sections) { + for (const step of section.steps) { + if (step.citation?.file_path) return step.citation; + } + } + return null; + }; + + // Ask the parent to open the right-side viewer at a citation's grounded range. + const handleCitationClick = (citation: CodemapCitation, data: CodemapData | null) => { + const files = data ? collectCodemapFiles(data) : [citation.file_path]; + onOpenCodeViewer?.( + { + file_path: citation.file_path, + start_line: citation.start_line, + end_line: citation.end_line, + snippet: citation.snippet, + }, + files + ); + }; + + // Generate a codemap over the WebSocket NDJSON stream. + const handleCodemapAsk = () => { + const askedQuestion = question; + setIsLoading(true); + setQuestion(''); + setCodemapActive(true); + setCodemapData(null); + setCodemapError(null); + setCodemapQuestion(askedQuestion); + setCodemapPhaseStatus({ analyzing: 'active', initial_codemap: 'pending', diagrams: 'pending' }); + // Full-width progress while generating; the viewer auto-opens only once a + // codemap with citations arrives. + onCloseCodeViewer?.(); + + const request: CodemapRequest = { + repo_url: getRepoUrl(repoInfo), + question: askedQuestion, + type: repoInfo.type, + provider: selectedProvider, + model: isCustomSelectedModel ? customSelectedModel : selectedModel, + language, + }; + if (repoInfo?.token) request.token = repoInfo.token; + + closeWebSocket(webSocketRef.current); + let finalData: CodemapData | null = null; + + webSocketRef.current = createCodemapWebSocket(request, { + onEvent: (evt) => { + if (evt.type === 'phase') { + setCodemapPhaseStatus((prev) => ({ + ...prev, + [evt.phase]: evt.status === 'done' ? 'done' : 'active', + })); + } else if (evt.type === 'codemap') { + finalData = evt.data; + setCodemapData(evt.data); + // Auto-open the code viewer at the first citation, if any. + const first = firstCitation(evt.data); + if (first) { + onOpenCodeViewer?.( + { + file_path: first.file_path, + start_line: first.start_line, + end_line: first.end_line, + snippet: first.snippet, + }, + collectCodemapFiles(evt.data) + ); + } + } else if (evt.type === 'error') { + setCodemapError(evt.message); + } + }, + onError: () => { + setCodemapError('Connection failed. Please try again.'); + setIsLoading(false); + }, + onClose: () => { + setIsLoading(false); + setCodemapActive(false); + if (finalData) { + setConversationTurns((prev) => [ + ...prev, + { kind: 'codemap', question: askedQuestion, data: finalData! }, + ]); + setCodemapData(null); + setCodemapQuestion(''); + setCodemapPhaseStatus(IDLE_PHASES); + } + }, + }); + }; + // Handle confirm and send request const handleConfirmAsk = async () => { + // Capture the question for this turn and clear the input so the user + // can immediately type a follow-up. + const askedQuestion = question; + const askedMode: 'normal' | 'deep_research' = deepResearch ? 'deep_research' : 'normal'; + + fallbackActiveRef.current = false; setIsLoading(true); setResponse(''); + setCurrentQuestion(askedQuestion); + setQuestion(''); setResearchIteration(0); setResearchComplete(false); + setResearchStages([]); + setCurrentStageIndex(0); + // Non-codemap answers use the full width, so make sure the viewer is closed. + onCloseCodeViewer?.(); try { // Create initial message const initialMessage: Message = { role: 'user', - content: question, - mode: deepResearch ? 'deep_research' : 'normal' + content: askedQuestion, + mode: askedMode }; - // Set initial conversation history - const newHistory: Message[] = [initialMessage]; + // Include previously completed turns so the backend keeps context, + // then append the new question. + const newHistory: Message[] = [...buildHistoryFromTurns(), initialMessage]; setConversationHistory(newHistory); // Prepare request body @@ -612,20 +882,23 @@ const Ask: React.FC = ({ setResponse(prev => prev + '\n\nError: WebSocket connection failed. Falling back to HTTP...'); // Fallback to HTTP if WebSocket fails - fallbackToHttp(requestBody); + fallbackActiveRef.current = true; + fallbackToHttp(requestBody, askedQuestion, askedMode); }, // Close handler () => { + if (fallbackActiveRef.current) return; // If deep research is enabled, check if we should continue if (deepResearch) { - const isComplete = checkIfResearchComplete(fullResponse); - setResearchComplete(isComplete); - - // If not complete, start the research process - if (!isComplete) { - setResearchIteration(1); - // The continueResearch function will be triggered by the useEffect - } + // Iteration 1 is the initial research plan — always continue into + // the deeper iterations instead of letting the completion + // heuristic (which can false-positive on the plan text) stop here. + setResearchComplete(false); + setResearchIteration(1); + // The continueResearch function will be triggered by the useEffect. + } else { + // Normal mode: the answer is final, so append it to the log. + commitTurn(askedQuestion, fullResponse, 'normal'); } setIsLoading(false); @@ -639,6 +912,57 @@ const Ask: React.FC = ({ } }; + // Move a finished exchange out of the live response area and into the + // persistent conversation log. + const commitTurn = ( + turnQuestion: string, + turnResponse: string, + mode: 'normal' | 'deep_research', + stages: ResearchStage[] = [] + ) => { + if (!turnQuestion && !turnResponse) return; + + // For deep research, make sure the final answer is always present as the + // last stage (stage extraction can miss it, e.g. on forced completion). + let finalStages = stages; + if (mode === 'deep_research') { + const last = stages[stages.length - 1]; + if (turnResponse && (!last || last.content !== turnResponse)) { + finalStages = [ + ...stages, + { + title: 'Final Result', + content: turnResponse, + iteration: stages.length + 1, + type: 'conclusion', + }, + ]; + } + } + + setConversationTurns(prev => [...prev, { + kind: 'chat', + question: turnQuestion, + response: turnResponse, + mode, + researchStages: finalStages, + stageIndex: finalStages.length > 0 ? finalStages.length - 1 : 0, + }]); + setResponse(''); + setCurrentQuestion(''); + setResearchStages([]); + setCurrentStageIndex(0); + }; + + // Switch which research stage is shown for a committed deep research turn. + const setTurnStage = (turnIndex: number, stageIndex: number) => { + setConversationTurns(prev => + prev.map((turn, i) => + i === turnIndex && turn.kind === 'chat' ? { ...turn, stageIndex } : turn + ) + ); + }; + const [buttonWidth, setButtonWidth] = useState(0); const buttonRef = useRef(null); @@ -651,170 +975,196 @@ const Ask: React.FC = ({ }, [messages.ask?.askButton, isLoading]); return ( -
-
-
- {/* Model selection button */} - -
+
+ {/* Conversation log: previous turns are preserved and new results + are appended below as their own sections. */} +
+ {conversationTurns.map((turn, idx) => { + // Codemap turn: render the codemap card in its chronological position. + if (turn.kind === 'codemap') { + return ( +
+
+
+ {turn.question} +
+
+ handleCitationClick(c, turn.data)} + /> +
+ ); + } - {/* Question input */} -
-
- setQuestion(e.target.value)} - placeholder={messages.ask?.placeholder || 'What would you like to know about this codebase?'} - className="block w-full rounded-md border border-[var(--border-color)] bg-[var(--input-bg)] text-[var(--foreground)] px-5 py-3.5 text-base shadow-sm focus:border-[var(--accent-primary)] focus:ring-2 focus:ring-[var(--accent-primary)]/30 focus:outline-none transition-all" - style={{ paddingRight: `${buttonWidth + 24}px` }} - disabled={isLoading} - /> - -
+ // Chat turn (normal / deep research). + // Deep research turns keep every iteration; show the selected one + // and expose navigation. Normal turns just show their response. + const stages = turn.researchStages ?? []; + const hasStages = turn.mode === 'deep_research' && stages.length > 0; + const activeStage = hasStages + ? Math.min(turn.stageIndex ?? stages.length - 1, stages.length - 1) + : 0; + const displayContent = hasStages ? stages[activeStage].content : turn.response; + + return ( +
+ {/* User question */} +
+
+ {turn.question} +
+
- {/* Deep Research toggle */} -
-
-