From 66662768efc575a14feadc6fd59315171f92791e Mon Sep 17 00:00:00 2001 From: Hari John Kuriakose Date: Fri, 19 Dec 2025 21:28:58 +0530 Subject: [PATCH 1/5] * feat: add unstract adapter extension skill for claude * feat: add azure ai foundry adapter * feat: update parameters in various adapters --- .../unstract-adapter-extension/SKILL.md | 353 ++++++++++ .../templates/embedding_adapter.py.template | 59 ++ .../embedding_parameters.py.template | 57 ++ .../templates/embedding_schema.json.template | 52 ++ .../assets/templates/llm_adapter.py.template | 59 ++ .../templates/llm_parameters.py.template | 61 ++ .../assets/templates/llm_schema.json.template | 58 ++ .../references/adapter_patterns.md | 562 ++++++++++++++++ .../references/json_schema_guide.md | 549 +++++++++++++++ .../references/provider_capabilities.md | 145 ++++ .../scripts/check_adapter_updates.py | 369 +++++++++++ .../scripts/init_embedding_adapter.py | 613 +++++++++++++++++ .../scripts/init_llm_adapter.py | 626 ++++++++++++++++++ .../scripts/manage_models.py | 285 ++++++++ .../icons/adapter-icons/AzureAIFoundry.png | Bin 0 -> 31417 bytes .../sdk1/src/unstract/sdk1/adapters/base1.py | 87 ++- .../adapters/embedding1/static/azure.json | 9 +- .../adapters/embedding1/static/openai.json | 11 +- .../sdk1/adapters/llm1/azure_ai_foundry.py | 40 ++ .../llm1/static/azure_ai_foundry.json | 59 ++ .../sdk1/adapters/llm1/static/bedrock.json | 18 +- .../sdk1/adapters/llm1/static/mistral.json | 53 +- .../sdk1/adapters/llm1/static/ollama.json | 27 +- 23 files changed, 4133 insertions(+), 19 deletions(-) create mode 100644 .claude/skills/unstract-adapter-extension/SKILL.md create mode 100644 .claude/skills/unstract-adapter-extension/assets/templates/embedding_adapter.py.template create mode 100644 .claude/skills/unstract-adapter-extension/assets/templates/embedding_parameters.py.template create mode 100644 .claude/skills/unstract-adapter-extension/assets/templates/embedding_schema.json.template create mode 100644 .claude/skills/unstract-adapter-extension/assets/templates/llm_adapter.py.template create mode 100644 .claude/skills/unstract-adapter-extension/assets/templates/llm_parameters.py.template create mode 100644 .claude/skills/unstract-adapter-extension/assets/templates/llm_schema.json.template create mode 100644 .claude/skills/unstract-adapter-extension/references/adapter_patterns.md create mode 100644 .claude/skills/unstract-adapter-extension/references/json_schema_guide.md create mode 100644 .claude/skills/unstract-adapter-extension/references/provider_capabilities.md create mode 100644 .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py create mode 100755 .claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py create mode 100755 .claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py create mode 100755 .claude/skills/unstract-adapter-extension/scripts/manage_models.py create mode 100644 frontend/public/icons/adapter-icons/AzureAIFoundry.png create mode 100644 unstract/sdk1/src/unstract/sdk1/adapters/llm1/azure_ai_foundry.py create mode 100644 unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/azure_ai_foundry.json diff --git a/.claude/skills/unstract-adapter-extension/SKILL.md b/.claude/skills/unstract-adapter-extension/SKILL.md new file mode 100644 index 0000000000..3f91f2e4f3 --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/SKILL.md @@ -0,0 +1,353 @@ +--- +name: unstract-adapter-extension +description: Extend LLM and embedding adapters in unstract/sdk1. Use when adding new adapters (LLM or embedding), removing adapters, adding/removing models to existing adapters, or editing adapter configurations. Supports OpenAI-compatible providers, cloud providers (AWS Bedrock, VertexAI, Azure), and self-hosted models (Ollama). +--- + +# Unstract Adapter Extension Skill + +This skill provides workflows and automation for extending LLM and embedding adapters in the `unstract/sdk1` module. + +## Supported Operations + +| Operation | Command | Description | +|-----------|---------|-------------| +| Add LLM Adapter | `scripts/init_llm_adapter.py` | Create new LLM provider adapter | +| Add Embedding Adapter | `scripts/init_embedding_adapter.py` | Create new embedding provider adapter | +| Remove Adapter | Manual deletion | Remove adapter files and parameter class | +| Add/Remove Models | `scripts/manage_models.py` | Modify available models in JSON schema | +| Edit Adapter | Manual edit | Modify existing adapter behavior | +| Check for Updates | `scripts/check_adapter_updates.py` | Compare adapters against LiteLLM features | + +## Quick Reference + +### File Locations +``` +unstract/sdk1/src/unstract/sdk1/adapters/ +├── base1.py # Parameter classes (add new ones here) +├── llm1/ # LLM adapters +│ ├── {provider}.py # Adapter implementation +│ └── static/{provider}.json # UI schema +└── embedding1/ # Embedding adapters + ├── {provider}.py # Adapter implementation + └── static/{provider}.json # UI schema +``` + +### ID Format +Adapter IDs follow the pattern: `{provider}|{uuid4}` +- Example: `openai|502ecf49-e47c-445c-9907-6d4b90c5cd17` +- Generate UUID: `python -c "import uuid; print(uuid.uuid4())"` + +### Model Prefix Convention +LiteLLM requires provider prefixes on model names: +| Provider | Prefix | Example | +|----------|--------|---------| +| OpenAI | `openai/` | `openai/gpt-4` | +| Azure | `azure/` | `azure/gpt-4-deployment` | +| Anthropic | `anthropic/` | `anthropic/claude-3-opus` | +| Bedrock | `bedrock/` | `bedrock/anthropic.claude-v2` | +| VertexAI | `vertex_ai/` | `vertex_ai/gemini-pro` | +| Ollama | `ollama_chat/` | `ollama_chat/llama2` | +| Mistral | `mistral/` | `mistral/mistral-large` | +| Anyscale | `anyscale/` | `anyscale/meta-llama/Llama-2-70b` | + +## Workflows + +### Adding a New LLM Adapter + +1. **Run initialization script**: + ```bash + python .claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py \ + --provider newprovider \ + --name "New Provider" \ + --description "New Provider LLM adapter" \ + --auto-logo + ``` + + **Logo options**: + - `--auto-logo`: Search for potential logo sources (Clearbit, GitHub) and display suggestions. Does NOT auto-download - you must verify and use `--logo-url` to download. + - `--logo-url URL`: Download logo from a verified URL (supports SVG and raster images) + - `--logo-file PATH`: Copy logo from local file (supports SVG and raster images) + + **Logo image settings** (optimized for sharp rendering): + - SVG conversion: 4800 DPI density, 8-bit depth, 512x512 pixels + - Raster images: Resized to 512x512 with LANCZOS resampling + - Requires ImageMagick for SVG conversion (`sudo pacman -S imagemagick`) + + **GitHub logo URL tip**: When downloading logos from GitHub, always use the raw URL: + - ❌ `https://github.com/user/repo/blob/main/logo.svg` + - ✅ `https://raw.githubusercontent.com/user/repo/main/logo.svg` + + Logos are saved to: `frontend/public/icons/adapter-icons/{ProviderName}.png` + +2. **Add parameter class to `base1.py`** (if provider has unique parameters): + ```python + class NewProviderLLMParameters(BaseChatCompletionParameters): + """See https://docs.litellm.ai/docs/providers/newprovider.""" + + api_key: str + # Add provider-specific fields + + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + adapter_metadata["model"] = NewProviderLLMParameters.validate_model(adapter_metadata) + return NewProviderLLMParameters(**adapter_metadata).model_dump() + + @staticmethod + def validate_model(adapter_metadata: dict[str, "Any"]) -> str: + model = adapter_metadata.get("model", "") + if model.startswith("newprovider/"): + return model + return f"newprovider/{model}" + ``` + +3. **Update adapter class** to inherit from new parameter class: + ```python + from unstract.sdk1.adapters.base1 import BaseAdapter, NewProviderLLMParameters + + class NewProviderLLMAdapter(NewProviderLLMParameters, BaseAdapter): + # ... implementation + ``` + +4. **Customize JSON schema** in `llm1/static/newprovider.json` for UI configuration + +5. **Test the adapter**: + ```python + from unstract.sdk1.adapters.adapterkit import Adapterkit + kit = Adapterkit() + adapters = kit.get_adapters_list() + # Verify new adapter appears + ``` + +### Adding a New Embedding Adapter + +1. **Run initialization script**: + ```bash + python .claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py \ + --provider newprovider \ + --name "New Provider" \ + --description "New Provider embedding adapter" \ + --auto-logo + ``` + + Same logo options as LLM adapter: `--auto-logo` (search only), `--logo-url`, `--logo-file` + +2. **Add parameter class to `base1.py`** (if needed): + ```python + class NewProviderEmbeddingParameters(BaseEmbeddingParameters): + """See https://docs.litellm.ai/docs/providers/newprovider.""" + + api_key: str + embed_batch_size: int | None = 10 + + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + adapter_metadata["model"] = NewProviderEmbeddingParameters.validate_model(adapter_metadata) + return NewProviderEmbeddingParameters(**adapter_metadata).model_dump() + + @staticmethod + def validate_model(adapter_metadata: dict[str, "Any"]) -> str: + return adapter_metadata.get("model", "") + ``` + +3. **Update adapter class and JSON schema** + +### Removing an Adapter + +1. **Delete adapter file**: `llm1/{provider}.py` or `embedding1/{provider}.py` +2. **Delete JSON schema**: `llm1/static/{provider}.json` or `embedding1/static/{provider}.json` +3. **Remove parameter class** from `base1.py` (if dedicated class exists) +4. **Verify removal**: Run `Adapterkit().get_adapters_list()` to confirm + +### Adding/Removing Models from Existing Adapter + +1. **Edit JSON schema** (`static/{provider}.json`): + ```json + { + "properties": { + "model": { + "type": "string", + "title": "Model", + "default": "new-default-model", + "description": "Available models: model-1, model-2, model-3" + } + } + } + ``` + +2. **For dropdown selection**, use enum: + ```json + { + "properties": { + "model": { + "type": "string", + "title": "Model", + "enum": ["model-1", "model-2", "model-3"], + "default": "model-1" + } + } + } + ``` + +3. **Run management script** for automated updates: + ```bash + python .claude/skills/unstract-adapter-extension/scripts/manage_models.py \ + --adapter llm \ + --provider openai \ + --action add \ + --models "gpt-4-turbo,gpt-4o-mini" + ``` + +### Editing Adapter Behavior + +Common modifications: + +1. **Add reasoning/thinking support**: + - Add `enable_thinking` boolean field to JSON schema + - Add conditional `thinking` config in `validate()` method + - See `AnthropicLLMParameters` in `base1.py` for reference + +2. **Add custom field mapping**: + ```python + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + # Map custom field names to expected names + if "custom_field" in adapter_metadata: + adapter_metadata["expected_field"] = adapter_metadata["custom_field"] + # Continue validation... + ``` + +3. **Add conditional fields in JSON schema**: + ```json + { + "allOf": [ + { + "if": { "properties": { "feature_enabled": { "const": true } } }, + "then": { + "properties": { "feature_config": { "type": "string" } }, + "required": ["feature_config"] + } + } + ] + } + ``` + +### Checking for Adapter Updates + +Compare existing adapter schemas against known LiteLLM features to identify potential updates: + +1. **Run the update checker**: + ```bash + # Check all adapters + python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py + + # Check specific adapter type + python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py --adapter llm + python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py --adapter embedding + + # Check specific provider + python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py --provider openai + + # Output as JSON + python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py --json + ``` + +2. **Review the report**: + - 🟡 **NEEDS UPDATE**: Adapters with missing parameters or outdated features + - ✅ **UP TO DATE**: Adapters matching known LiteLLM features + - ❌ **ERRORS**: Adapters that couldn't be analyzed (missing schema, etc.) + +3. **Common update types identified**: + - **Missing parameters**: New configuration options (e.g., `dimensions` for embeddings) + - **Reasoning/Thinking support**: Enable reasoning for models like o1, o3, Claude 3.7+, Magistral + - **Outdated defaults**: Default models that have been superseded + +4. **After identifying updates**: + - Update JSON schema in `static/{provider}.json` + - Update parameter class in `base1.py` if validation logic changes + - Consult LiteLLM docs for implementation details (URLs provided in report) + +5. **Update the feature database** (`check_adapter_updates.py`): + - Edit `LITELLM_FEATURES` dict to add new providers or parameters + - Keep `known_params`, `reasoning_models`, `thinking_models`, `latest_models` current + - Add documentation URLs for reference + +## Validation Checklist + +Before submitting adapter changes: + +- [ ] Adapter class inherits from correct parameter class AND `BaseAdapter` +- [ ] `get_id()` returns unique `{provider}|{uuid}` format +- [ ] `get_metadata()` returns dict with `name`, `version`, `adapter`, `description`, `is_active` +- [ ] `get_provider()` returns lowercase provider identifier +- [ ] `get_adapter_type()` returns correct `AdapterTypes.LLM` or `AdapterTypes.EMBEDDING` +- [ ] JSON schema has `adapter_name` as required field +- [ ] `validate()` method adds correct model prefix +- [ ] `validate_model()` method handles prefix idempotently (doesn't double-prefix) +- [ ] All static methods decorated with `@staticmethod` +- [ ] Icon path follows pattern `/icons/adapter-icons/{Name}.png` + +## Maintenance Workflow + +Periodic maintenance to keep adapters current with LiteLLM features: + +### Monthly Update Check + +1. **Run the update checker**: + ```bash + python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py + ``` + +2. **Review LiteLLM changelog** for new provider features: + - https://github.com/BerriAI/litellm/releases + +3. **Update feature database** in `check_adapter_updates.py`: + - Add new `known_params` for each provider + - Update `reasoning_models` and `thinking_models` lists + - Update `latest_models` with current defaults + +4. **Apply updates** following priority: + - 🔴 **High**: Security or breaking changes + - 🟡 **Medium**: New capabilities (reasoning, dimensions) + - 🟢 **Low**: Model updates, documentation + +### After LiteLLM Upgrade + +When upgrading LiteLLM dependency: + +1. Check for **API changes** in provider parameters +2. Verify **model prefix** requirements haven't changed +3. Test **thinking/reasoning** features still work +4. Update **default models** if deprecated + +### Adding New Provider Support + +When LiteLLM adds a new provider: + +1. Check LiteLLM docs: `https://docs.litellm.ai/docs/providers/{provider}` +2. Add feature data to `check_adapter_updates.py` +3. Run init script to create adapter skeleton +4. Customize JSON schema and parameter class + +## Reference Files + +For detailed patterns and examples, see: +- `references/adapter_patterns.md` - Complete code patterns +- `references/json_schema_guide.md` - JSON schema patterns for UI +- `references/provider_capabilities.md` - Provider feature matrix +- `assets/templates/` - Ready-to-use templates + +## Troubleshooting + +### Adapter not appearing in list +- Verify class name ends with `LLMAdapter` or `EmbeddingAdapter` +- Check `is_active: True` in metadata +- Ensure file is in correct directory (`llm1/` or `embedding1/`) + +### Validation errors +- Check parameter class fields match JSON schema required fields +- Verify `validate()` returns properly validated dict +- Ensure model prefix logic is idempotent + +### Import errors +- Verify imports in adapter file match available classes in `base1.py` +- Check for circular imports (use `TYPE_CHECKING` guard) diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/embedding_adapter.py.template b/.claude/skills/unstract-adapter-extension/assets/templates/embedding_adapter.py.template new file mode 100644 index 0000000000..9fbaa0307f --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/assets/templates/embedding_adapter.py.template @@ -0,0 +1,59 @@ +"""Embedding Adapter Template for ${PROVIDER_NAME}. + +Replace placeholders: +- ${PROVIDER_NAME} -> Display name (e.g., "New Provider") +- ${PROVIDER_ID} -> Lowercase identifier (e.g., "newprovider") +- ${CLASS_NAME} -> PascalCase name (e.g., "NewProvider") +- ${UUID} -> Generate with: python -c "import uuid; print(uuid.uuid4())" +- ${PARAM_CLASS} -> Parameter class name (e.g., "NewProviderEmbeddingParameters") +""" + +from typing import Any + +from unstract.sdk1.adapters.base1 import BaseAdapter, ${PARAM_CLASS} +from unstract.sdk1.adapters.enums import AdapterTypes + + +class ${CLASS_NAME}EmbeddingAdapter(${PARAM_CLASS}, BaseAdapter): + """Embedding adapter for ${PROVIDER_NAME}.""" + + @staticmethod + def get_id() -> str: + """Return unique adapter ID.""" + return "${PROVIDER_ID}|${UUID}" + + @staticmethod + def get_metadata() -> dict[str, Any]: + """Return adapter metadata for registration.""" + return { + "name": "${PROVIDER_NAME}", + "version": "1.0.0", + "adapter": ${CLASS_NAME}EmbeddingAdapter, + "description": "${PROVIDER_NAME} embedding adapter", + "is_active": True, + } + + @staticmethod + def get_name() -> str: + """Return display name.""" + return "${PROVIDER_NAME}" + + @staticmethod + def get_description() -> str: + """Return description.""" + return "${PROVIDER_NAME} embedding adapter" + + @staticmethod + def get_provider() -> str: + """Return lowercase provider identifier.""" + return "${PROVIDER_ID}" + + @staticmethod + def get_icon() -> str: + """Return icon path.""" + return "/icons/adapter-icons/${PROVIDER_NAME}.png" + + @staticmethod + def get_adapter_type() -> AdapterTypes: + """Return adapter type.""" + return AdapterTypes.EMBEDDING diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/embedding_parameters.py.template b/.claude/skills/unstract-adapter-extension/assets/templates/embedding_parameters.py.template new file mode 100644 index 0000000000..466d723565 --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/assets/templates/embedding_parameters.py.template @@ -0,0 +1,57 @@ +"""Embedding Parameter Class Template for ${PROVIDER_NAME}. + +Add this class to base1.py after the existing parameter classes. + +Replace placeholders: +- ${PROVIDER_NAME} -> Display name (e.g., "New Provider") +- ${PROVIDER_ID} -> Lowercase identifier (e.g., "newprovider") +- ${CLASS_NAME} -> PascalCase name (e.g., "NewProvider") +""" + + +class ${CLASS_NAME}EmbeddingParameters(BaseEmbeddingParameters): + """Parameters for ${PROVIDER_NAME} Embedding. + + See https://docs.litellm.ai/docs/providers/${PROVIDER_ID} + """ + + # Required fields + api_key: str + + # Optional fields with defaults + api_base: str | None = None + embed_batch_size: int | None = 10 + + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + """Validate and transform adapter metadata. + + Args: + adapter_metadata: Raw configuration from UI form + + Returns: + Validated configuration dict for embedding provider + """ + # Set model (embedding models often don't need prefix) + adapter_metadata["model"] = ${CLASS_NAME}EmbeddingParameters.validate_model( + adapter_metadata + ) + + # Map any custom field names here + # Example: if "endpoint" in adapter_metadata: + # adapter_metadata["api_base"] = adapter_metadata["endpoint"] + + # Validate with pydantic and return + return ${CLASS_NAME}EmbeddingParameters(**adapter_metadata).model_dump() + + @staticmethod + def validate_model(adapter_metadata: dict[str, "Any"]) -> str: + """Get model name (embedding models typically don't need prefix). + + Args: + adapter_metadata: Configuration containing 'model' key + + Returns: + Model name (usually without prefix for embeddings) + """ + return adapter_metadata.get("model", "") diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/embedding_schema.json.template b/.claude/skills/unstract-adapter-extension/assets/templates/embedding_schema.json.template new file mode 100644 index 0000000000..65e66a3323 --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/assets/templates/embedding_schema.json.template @@ -0,0 +1,52 @@ +{ + "title": "${PROVIDER_NAME} Embedding", + "type": "object", + "required": [ + "adapter_name", + "api_key" + ], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name", + "default": "", + "description": "Provide a unique name for this adapter instance. Example: ${PROVIDER_ID}-emb-1" + }, + "model": { + "type": "string", + "title": "Model", + "default": "", + "description": "The embedding model to use." + }, + "api_key": { + "type": "string", + "title": "API Key", + "default": "", + "format": "password", + "description": "Your ${PROVIDER_NAME} API key." + }, + "api_base": { + "type": "string", + "title": "API Base", + "format": "uri", + "default": "", + "description": "API endpoint URL (optional)." + }, + "embed_batch_size": { + "type": "number", + "minimum": 1, + "multipleOf": 1, + "title": "Embed Batch Size", + "default": 10, + "description": "Number of texts to embed per batch." + }, + "timeout": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Timeout", + "default": 240, + "description": "Request timeout in seconds." + } + } +} diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/llm_adapter.py.template b/.claude/skills/unstract-adapter-extension/assets/templates/llm_adapter.py.template new file mode 100644 index 0000000000..b042e12ffe --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/assets/templates/llm_adapter.py.template @@ -0,0 +1,59 @@ +"""LLM Adapter Template for ${PROVIDER_NAME}. + +Replace placeholders: +- ${PROVIDER_NAME} -> Display name (e.g., "New Provider") +- ${PROVIDER_ID} -> Lowercase identifier (e.g., "newprovider") +- ${CLASS_NAME} -> PascalCase name (e.g., "NewProvider") +- ${UUID} -> Generate with: python -c "import uuid; print(uuid.uuid4())" +- ${PARAM_CLASS} -> Parameter class name (e.g., "NewProviderLLMParameters") +""" + +from typing import Any + +from unstract.sdk1.adapters.base1 import BaseAdapter, ${PARAM_CLASS} +from unstract.sdk1.adapters.enums import AdapterTypes + + +class ${CLASS_NAME}LLMAdapter(${PARAM_CLASS}, BaseAdapter): + """LLM adapter for ${PROVIDER_NAME}.""" + + @staticmethod + def get_id() -> str: + """Return unique adapter ID.""" + return "${PROVIDER_ID}|${UUID}" + + @staticmethod + def get_metadata() -> dict[str, Any]: + """Return adapter metadata for registration.""" + return { + "name": "${PROVIDER_NAME}", + "version": "1.0.0", + "adapter": ${CLASS_NAME}LLMAdapter, + "description": "${PROVIDER_NAME} LLM adapter", + "is_active": True, + } + + @staticmethod + def get_name() -> str: + """Return display name.""" + return "${PROVIDER_NAME}" + + @staticmethod + def get_description() -> str: + """Return description.""" + return "${PROVIDER_NAME} LLM adapter" + + @staticmethod + def get_provider() -> str: + """Return lowercase provider identifier.""" + return "${PROVIDER_ID}" + + @staticmethod + def get_icon() -> str: + """Return icon path.""" + return "/icons/adapter-icons/${PROVIDER_NAME}.png" + + @staticmethod + def get_adapter_type() -> AdapterTypes: + """Return adapter type.""" + return AdapterTypes.LLM diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/llm_parameters.py.template b/.claude/skills/unstract-adapter-extension/assets/templates/llm_parameters.py.template new file mode 100644 index 0000000000..ea26f69ae1 --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/assets/templates/llm_parameters.py.template @@ -0,0 +1,61 @@ +"""LLM Parameter Class Template for ${PROVIDER_NAME}. + +Add this class to base1.py after the existing parameter classes. + +Replace placeholders: +- ${PROVIDER_NAME} -> Display name (e.g., "New Provider") +- ${PROVIDER_ID} -> Lowercase identifier (e.g., "newprovider") +- ${CLASS_NAME} -> PascalCase name (e.g., "NewProvider") +- ${MODEL_PREFIX} -> LiteLLM model prefix (e.g., "newprovider/") +""" + + +class ${CLASS_NAME}LLMParameters(BaseChatCompletionParameters): + """Parameters for ${PROVIDER_NAME} LLM. + + See https://docs.litellm.ai/docs/providers/${PROVIDER_ID} + """ + + # Required fields + api_key: str + + # Optional fields with defaults + api_base: str | None = None + + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + """Validate and transform adapter metadata. + + Args: + adapter_metadata: Raw configuration from UI form + + Returns: + Validated configuration dict for LiteLLM + """ + # Add model prefix + adapter_metadata["model"] = ${CLASS_NAME}LLMParameters.validate_model( + adapter_metadata + ) + + # Map any custom field names here + # Example: if "endpoint" in adapter_metadata: + # adapter_metadata["api_base"] = adapter_metadata["endpoint"] + + # Validate with pydantic and return + return ${CLASS_NAME}LLMParameters(**adapter_metadata).model_dump() + + @staticmethod + def validate_model(adapter_metadata: dict[str, "Any"]) -> str: + """Add provider prefix to model name (idempotent). + + Args: + adapter_metadata: Configuration containing 'model' key + + Returns: + Model name with provider prefix + """ + model = adapter_metadata.get("model", "") + # Avoid double-prefixing + if model.startswith("${MODEL_PREFIX}"): + return model + return f"${MODEL_PREFIX}{model}" diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/llm_schema.json.template b/.claude/skills/unstract-adapter-extension/assets/templates/llm_schema.json.template new file mode 100644 index 0000000000..b17f2ea1b7 --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/assets/templates/llm_schema.json.template @@ -0,0 +1,58 @@ +{ + "title": "${PROVIDER_NAME} LLM", + "type": "object", + "required": [ + "adapter_name", + "api_key" + ], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name", + "default": "", + "description": "Provide a unique name for this adapter instance. Example: ${PROVIDER_ID}-llm-1" + }, + "api_key": { + "type": "string", + "title": "API Key", + "format": "password", + "description": "Your ${PROVIDER_NAME} API key." + }, + "model": { + "type": "string", + "title": "Model", + "default": "", + "description": "The model to use for API requests." + }, + "api_base": { + "type": "string", + "format": "url", + "title": "API Base", + "default": "", + "description": "API endpoint URL (optional, uses default if not specified)." + }, + "max_tokens": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Maximum Output Tokens", + "description": "Maximum number of output tokens to limit LLM replies." + }, + "max_retries": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Max Retries", + "default": 5, + "description": "Maximum retry attempts for failed requests." + }, + "timeout": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Timeout", + "default": 900, + "description": "Request timeout in seconds." + } + } +} diff --git a/.claude/skills/unstract-adapter-extension/references/adapter_patterns.md b/.claude/skills/unstract-adapter-extension/references/adapter_patterns.md new file mode 100644 index 0000000000..651d0944ba --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/references/adapter_patterns.md @@ -0,0 +1,562 @@ +# Adapter Patterns Reference + +Complete code patterns for extending unstract/sdk1 adapters. + +## Architecture Overview + +``` +unstract/sdk1/src/unstract/sdk1/adapters/ +├── base1.py # Parameter classes & base adapter +│ ├── register_adapters() # Auto-discovery function +│ ├── BaseAdapter # Abstract base class +│ ├── BaseChatCompletionParameters # LLM base params +│ ├── BaseEmbeddingParameters # Embedding base params +│ └── {Provider}Parameters # Provider-specific params +├── adapterkit.py # Singleton registry (Adapterkit) +├── enums.py # AdapterTypes enum +├── llm1/ # LLM adapters +│ ├── __init__.py # Calls register_adapters() +│ ├── {provider}.py # Adapter implementations +│ └── static/ # JSON schemas +└── embedding1/ # Embedding adapters + ├── __init__.py # Calls register_adapters() + ├── {provider}.py # Adapter implementations + └── static/ # JSON schemas +``` + +## Registration Flow + +1. `llm1/__init__.py` imports and calls `register_adapters(adapters, "LLM")` +2. `register_adapters()` scans `llm1/*.py` files +3. For each file, inspects classes ending with `LLMAdapter` +4. Checks for `get_id()` and `get_metadata()` methods +5. Stores adapter in global dict: `adapters[adapter_id] = {module, metadata}` +6. `Adapterkit` singleton merges all adapter types on init + +## Complete LLM Adapter Example + +### Parameter Class (base1.py) + +```python +class NewProviderLLMParameters(BaseChatCompletionParameters): + """Provider-specific parameters. + + See https://docs.litellm.ai/docs/providers/newprovider + """ + + # Required fields (no defaults) + api_key: str + + # Optional fields (with defaults) + api_base: str | None = None + custom_header: str | None = None + + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + """Transform and validate adapter metadata. + + This method: + 1. Applies model prefix via validate_model() + 2. Maps field names if needed (e.g., azure_endpoint -> api_base) + 3. Handles special features (reasoning, thinking) + 4. Validates with pydantic and returns clean dict + """ + # Always set model with proper prefix + adapter_metadata["model"] = NewProviderLLMParameters.validate_model( + adapter_metadata + ) + + # Map custom field names to expected names + if "endpoint" in adapter_metadata and not adapter_metadata.get("api_base"): + adapter_metadata["api_base"] = adapter_metadata["endpoint"] + + # Validate with pydantic and return + return NewProviderLLMParameters(**adapter_metadata).model_dump() + + @staticmethod + def validate_model(adapter_metadata: dict[str, "Any"]) -> str: + """Add provider prefix to model name (idempotent). + + IMPORTANT: Must handle already-prefixed models to avoid double-prefixing. + """ + model = adapter_metadata.get("model", "") + if model.startswith("newprovider/"): + return model + return f"newprovider/{model}" +``` + +### Adapter Class (llm1/newprovider.py) + +```python +from typing import Any + +from unstract.sdk1.adapters.base1 import BaseAdapter, NewProviderLLMParameters +from unstract.sdk1.adapters.enums import AdapterTypes + + +class NewProviderLLMAdapter(NewProviderLLMParameters, BaseAdapter): + """LLM adapter for New Provider. + + Multiple inheritance order matters: + 1. Parameter class first (provides validate methods) + 2. BaseAdapter second (provides abstract methods) + """ + + @staticmethod + def get_id() -> str: + """Return unique adapter ID in format: provider|uuid4.""" + return "newprovider|a1b2c3d4-e5f6-7890-abcd-ef1234567890" + + @staticmethod + def get_metadata() -> dict[str, Any]: + """Return adapter metadata for registration.""" + return { + "name": "New Provider", + "version": "1.0.0", + "adapter": NewProviderLLMAdapter, # Reference to this class + "description": "New Provider LLM adapter", + "is_active": True, # Must be True for auto-registration + } + + @staticmethod + def get_name() -> str: + """Return display name.""" + return "New Provider" + + @staticmethod + def get_description() -> str: + """Return description.""" + return "New Provider LLM adapter" + + @staticmethod + def get_provider() -> str: + """Return lowercase provider identifier (used for schema path).""" + return "newprovider" + + @staticmethod + def get_icon() -> str: + """Return icon path (relative to frontend assets).""" + return "/icons/adapter-icons/NewProvider.png" + + @staticmethod + def get_adapter_type() -> AdapterTypes: + """Return adapter type enum.""" + return AdapterTypes.LLM +``` + +### JSON Schema (llm1/static/newprovider.json) + +```json +{ + "title": "New Provider LLM", + "type": "object", + "required": ["adapter_name", "api_key"], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name", + "default": "", + "description": "Unique name for this adapter instance" + }, + "api_key": { + "type": "string", + "title": "API Key", + "format": "password", + "description": "Your New Provider API key" + }, + "model": { + "type": "string", + "title": "Model", + "default": "default-model", + "description": "Model to use" + }, + "max_tokens": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Maximum Output Tokens" + }, + "timeout": { + "type": "number", + "minimum": 0, + "default": 900, + "title": "Timeout (seconds)" + } + } +} +``` + +## Reasoning Configuration Pattern (OpenAI o1/o3, Mistral Magistral) + +For providers supporting reasoning effort control: + +### JSON Schema Addition + +```json +{ + "properties": { + "enable_reasoning": { + "type": "boolean", + "title": "Enable Reasoning", + "default": false, + "description": "Enable reasoning capabilities for supported models" + } + }, + "allOf": [ + { + "if": { + "properties": { "enable_reasoning": { "const": true } } + }, + "then": { + "properties": { + "reasoning_effort": { + "type": "string", + "enum": ["low", "medium", "high"], + "default": "medium", + "title": "Reasoning Effort", + "description": "Controls depth of reasoning" + } + }, + "required": ["reasoning_effort"] + } + }, + { + "if": { + "properties": { "enable_reasoning": { "const": false } } + }, + "then": { + "properties": {} + } + } + ] +} +``` + +### Parameter Class Pattern + +```python +@staticmethod +def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + adapter_metadata["model"] = MyParameters.validate_model(adapter_metadata) + + # Handle reasoning configuration + enable_reasoning = adapter_metadata.get("enable_reasoning", False) + reasoning_effort = adapter_metadata.get("reasoning_effort") + + # Exclude control fields before validation + validation_metadata = { + k: v for k, v in adapter_metadata.items() + if k not in ("enable_reasoning", "reasoning_effort") + } + + validated = MyParameters(**validation_metadata).model_dump() + + # Add reasoning_effort back if enabled + if enable_reasoning and reasoning_effort: + validated["reasoning_effort"] = reasoning_effort + + return validated +``` + +## Thinking Configuration Pattern (Anthropic, VertexAI, Bedrock) + +For providers supporting extended thinking: + +### JSON Schema Addition + +```json +{ + "properties": { + "enable_thinking": { + "type": "boolean", + "title": "Enable Extended Thinking", + "default": false, + "description": "Allow extra reasoning for complex tasks" + } + }, + "allOf": [ + { + "if": { + "properties": { "enable_thinking": { "const": true } } + }, + "then": { + "properties": { + "budget_tokens": { + "type": "number", + "minimum": 1000, + "default": 10000, + "title": "Thinking Budget (tokens)" + } + } + } + } + ] +} +``` + +### Parameter Class Pattern + +```python +@staticmethod +def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + adapter_metadata["model"] = MyParameters.validate_model(adapter_metadata) + + # Handle thinking configuration + enable_thinking = adapter_metadata.get("enable_thinking", False) + + # Check if thinking was previously enabled + has_thinking_config = ( + "thinking" in adapter_metadata + and adapter_metadata.get("thinking") is not None + ) + if not enable_thinking and has_thinking_config: + enable_thinking = True + + result_metadata = adapter_metadata.copy() + + if enable_thinking: + if has_thinking_config: + result_metadata["thinking"] = adapter_metadata["thinking"] + else: + thinking_config = {"type": "enabled"} + budget_tokens = adapter_metadata.get("budget_tokens") + if budget_tokens is not None: + thinking_config["budget_tokens"] = budget_tokens + result_metadata["thinking"] = thinking_config + result_metadata["temperature"] = 1 # Required for thinking + + # Exclude control fields from validation + validation_metadata = { + k: v for k, v in result_metadata.items() + if k not in ("enable_thinking", "budget_tokens", "thinking") + } + + validated = MyParameters(**validation_metadata).model_dump() + + # Add thinking config back if enabled + if enable_thinking and "thinking" in result_metadata: + validated["thinking"] = result_metadata["thinking"] + + return validated +``` + +## Conditional Fields Pattern + +For fields that appear based on other field values: + +```json +{ + "properties": { + "deployment_type": { + "type": "string", + "enum": ["cloud", "on-premise"], + "default": "cloud" + } + }, + "allOf": [ + { + "if": { + "properties": { "deployment_type": { "const": "cloud" } } + }, + "then": { + "properties": { + "region": { + "type": "string", + "enum": ["us-east-1", "us-west-2", "eu-west-1"] + } + }, + "required": ["region"] + } + }, + { + "if": { + "properties": { "deployment_type": { "const": "on-premise" } } + }, + "then": { + "properties": { + "server_url": { + "type": "string", + "format": "uri", + "title": "Server URL" + } + }, + "required": ["server_url"] + } + } + ] +} +``` + +## Optional Credentials Pattern (AWS Bedrock) + +For providers with multiple authentication methods (credentials, SSO profile, IAM role): + +### JSON Schema + +```json +{ + "required": ["adapter_name", "region_name", "model"], + "properties": { + "aws_access_key_id": { + "type": "string", + "title": "AWS Access Key ID", + "format": "password", + "description": "Leave empty if using AWS Profile or IAM role." + }, + "aws_secret_access_key": { + "type": "string", + "title": "AWS Secret Access Key", + "format": "password", + "description": "Leave empty if using AWS Profile or IAM role." + }, + "aws_profile_name": { + "type": "string", + "title": "AWS Profile Name", + "description": "AWS SSO profile name. Use instead of access keys." + } + } +} +``` + +### Parameter Class Pattern + +```python +class AWSBedrockLLMParameters(BaseChatCompletionParameters): + # All credential fields are optional + aws_access_key_id: str | None = None + aws_secret_access_key: str | None = None + aws_profile_name: str | None = None + region_name: str # Required +``` + +## JSON Mode Pattern (Ollama) + +For providers supporting structured JSON output: + +### JSON Schema + +```json +{ + "properties": { + "json_mode": { + "type": "boolean", + "title": "JSON Mode", + "default": false, + "description": "Enable JSON mode to constrain output to valid JSON." + } + } +} +``` + +### Parameter Class Pattern + +```python +@staticmethod +def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + # Convert json_mode to response_format + json_mode = adapter_metadata.pop("json_mode", False) + + validated = OllamaLLMParameters(**adapter_metadata).model_dump() + + if json_mode: + validated["response_format"] = {"type": "json_object"} + + return validated +``` + +## Embedding Adapter Pattern + +Embedding adapters follow the same structure but with different base classes: + +```python +class NewProviderEmbeddingParameters(BaseEmbeddingParameters): + """Embedding-specific parameters.""" + + api_key: str + embed_batch_size: int | None = 10 + + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + adapter_metadata["model"] = NewProviderEmbeddingParameters.validate_model( + adapter_metadata + ) + return NewProviderEmbeddingParameters(**adapter_metadata).model_dump() + + @staticmethod + def validate_model(adapter_metadata: dict[str, "Any"]) -> str: + # Embedding models often don't need prefix + return adapter_metadata.get("model", "") + + +class NewProviderEmbeddingAdapter(NewProviderEmbeddingParameters, BaseAdapter): + # Same structure as LLM adapter but with: + # - get_adapter_type() returns AdapterTypes.EMBEDDING + # - Different UUID in get_id() +``` + +## Embedding Dimensions Pattern (OpenAI, Azure) + +For embedding models supporting custom output dimensions (text-embedding-3-*): + +### JSON Schema + +```json +{ + "properties": { + "dimensions": { + "type": "number", + "minimum": 1, + "multipleOf": 1, + "title": "Dimensions", + "description": "Output embedding dimensions. Only supported by text-embedding-3-* models. Leave empty for default." + } + } +} +``` + +### Parameter Class Pattern + +```python +class OpenAIEmbeddingParameters(BaseEmbeddingParameters): + api_key: str + dimensions: int | None = None # Optional, model-dependent +``` + +## Testing Adapters + +```python +from unstract.sdk1.adapters.adapterkit import Adapterkit + +# Get singleton instance +kit = Adapterkit() + +# List all adapters +adapters = kit.get_adapters_list() +for adapter_id, info in adapters.items(): + print(f"{adapter_id}: {info['metadata']['name']}") + +# Get specific adapter class +adapter_class = kit.get_adapter_class_by_adapter_id( + "newprovider|a1b2c3d4-e5f6-7890-abcd-ef1234567890" +) + +# Validate metadata +validated = adapter_class.validate({ + "model": "my-model", + "api_key": "sk-xxx", +}) +print(validated) + +# Get JSON schema +schema = adapter_class.get_json_schema() +print(schema) +``` + +## Common Mistakes + +1. **Missing `@staticmethod` decorator** - All adapter methods must be static +2. **Wrong class name suffix** - Must end with `LLMAdapter` or `EmbeddingAdapter` +3. **Double prefix in validate_model** - Always check if prefix exists first +4. **Missing `is_active: True`** - Adapter won't be registered without it +5. **Wrong inheritance order** - Parameter class must come before BaseAdapter +6. **Incorrect provider in get_provider()** - Must match filename and schema path diff --git a/.claude/skills/unstract-adapter-extension/references/json_schema_guide.md b/.claude/skills/unstract-adapter-extension/references/json_schema_guide.md new file mode 100644 index 0000000000..7f3def2109 --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/references/json_schema_guide.md @@ -0,0 +1,549 @@ +# JSON Schema Guide for Adapter UI + +Reference for creating JSON schemas that generate adapter configuration UIs. + +## Schema Structure + +```json +{ + "title": "Provider Name Type", + "type": "object", + "required": ["field1", "field2"], + "properties": { ... }, + "allOf": [ ... ] +} +``` + +## Field Types + +### String Field + +```json +{ + "field_name": { + "type": "string", + "title": "Display Label", + "default": "default value", + "description": "Help text shown to user" + } +} +``` + +### Password Field + +```json +{ + "api_key": { + "type": "string", + "title": "API Key", + "format": "password", + "description": "Your secret API key" + } +} +``` + +### URL Field + +```json +{ + "endpoint": { + "type": "string", + "title": "Endpoint URL", + "format": "uri", + "default": "https://api.example.com/v1" + } +} +``` + +### Number Field + +```json +{ + "timeout": { + "type": "number", + "title": "Timeout", + "default": 300, + "minimum": 0, + "maximum": 3600, + "multipleOf": 1, + "description": "Timeout in seconds" + } +} +``` + +### Integer Field + +```json +{ + "max_retries": { + "type": "integer", + "title": "Max Retries", + "default": 3, + "minimum": 0, + "maximum": 10 + } +} +``` + +### Boolean Field + +```json +{ + "enable_feature": { + "type": "boolean", + "title": "Enable Feature", + "default": false, + "description": "Toggle to enable this feature" + } +} +``` + +### Dropdown (Enum) Field + +```json +{ + "model": { + "type": "string", + "title": "Model", + "enum": ["model-a", "model-b", "model-c"], + "default": "model-a", + "description": "Select the model to use" + } +} +``` + +### Multi-line Text + +```json +{ + "json_credentials": { + "type": "string", + "title": "JSON Credentials", + "format": "textarea", + "description": "Paste your JSON credentials here" + } +} +``` + +## Required Fields + +Always include `adapter_name` as required: + +```json +{ + "required": ["adapter_name", "api_key"], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name", + "default": "", + "description": "Provide a unique name for this adapter instance" + } + } +} +``` + +## Conditional Fields + +Show/hide fields based on other field values using `allOf` with `if`/`then`: + +### Basic Conditional + +```json +{ + "properties": { + "auth_type": { + "type": "string", + "enum": ["api_key", "oauth"], + "default": "api_key" + } + }, + "allOf": [ + { + "if": { + "properties": { + "auth_type": { "const": "api_key" } + } + }, + "then": { + "properties": { + "api_key": { + "type": "string", + "format": "password", + "title": "API Key" + } + }, + "required": ["api_key"] + } + }, + { + "if": { + "properties": { + "auth_type": { "const": "oauth" } + } + }, + "then": { + "properties": { + "client_id": { "type": "string", "title": "Client ID" }, + "client_secret": { "type": "string", "format": "password" } + }, + "required": ["client_id", "client_secret"] + } + } + ] +} +``` + +### Boolean Toggle Conditional + +```json +{ + "properties": { + "enable_reasoning": { + "type": "boolean", + "default": false, + "title": "Enable Reasoning" + } + }, + "allOf": [ + { + "if": { + "properties": { + "enable_reasoning": { "const": true } + } + }, + "then": { + "properties": { + "reasoning_effort": { + "type": "string", + "enum": ["low", "medium", "high"], + "default": "medium", + "title": "Reasoning Effort" + } + }, + "required": ["reasoning_effort"] + } + }, + { + "if": { + "properties": { + "enable_reasoning": { "const": false } + } + }, + "then": { + "properties": {} + } + } + ] +} +``` + +## Complete Examples + +### Simple LLM Adapter Schema + +```json +{ + "title": "Simple Provider LLM", + "type": "object", + "required": ["adapter_name", "api_key"], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name", + "default": "", + "description": "Unique name for this adapter" + }, + "api_key": { + "type": "string", + "title": "API Key", + "format": "password" + }, + "model": { + "type": "string", + "title": "Model", + "default": "default-model" + }, + "max_tokens": { + "type": "number", + "minimum": 0, + "title": "Max Tokens" + }, + "timeout": { + "type": "number", + "minimum": 0, + "default": 900, + "title": "Timeout (seconds)" + } + } +} +``` + +### Cloud Provider with Regions + +```json +{ + "title": "Cloud Provider LLM", + "type": "object", + "required": ["adapter_name", "api_key", "region"], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name" + }, + "api_key": { + "type": "string", + "format": "password", + "title": "API Key" + }, + "region": { + "type": "string", + "title": "Region", + "enum": ["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"], + "default": "us-east-1" + }, + "model": { + "type": "string", + "title": "Model", + "enum": ["model-small", "model-medium", "model-large"], + "default": "model-medium" + } + } +} +``` + +### Azure-Style with Deployment + +```json +{ + "title": "Azure-Style Provider", + "type": "object", + "required": ["adapter_name", "api_key", "azure_endpoint", "deployment_name"], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name" + }, + "azure_endpoint": { + "type": "string", + "format": "uri", + "title": "Endpoint", + "description": "Your Azure endpoint URL" + }, + "api_key": { + "type": "string", + "format": "password", + "title": "API Key" + }, + "deployment_name": { + "type": "string", + "title": "Deployment Name", + "description": "Name of your model deployment" + }, + "api_version": { + "type": "string", + "title": "API Version", + "default": "2024-02-01" + } + } +} +``` + +### Self-Hosted (Ollama-Style) + +```json +{ + "title": "Self-Hosted LLM", + "type": "object", + "required": ["adapter_name", "base_url"], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name" + }, + "base_url": { + "type": "string", + "format": "uri", + "title": "Server URL", + "default": "http://localhost:11434", + "description": "URL of your local server" + }, + "model": { + "type": "string", + "title": "Model", + "default": "llama2", + "description": "Model name (must be pulled on server)" + } + } +} +``` + +### Embedding Adapter Schema + +```json +{ + "title": "Provider Embedding", + "type": "object", + "required": ["adapter_name", "api_key"], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name" + }, + "model": { + "type": "string", + "title": "Model", + "default": "text-embedding-model" + }, + "api_key": { + "type": "string", + "format": "password", + "title": "API Key" + }, + "api_base": { + "type": "string", + "format": "uri", + "title": "API Base URL" + }, + "embed_batch_size": { + "type": "number", + "minimum": 1, + "default": 10, + "title": "Batch Size" + }, + "timeout": { + "type": "number", + "minimum": 0, + "default": 240, + "title": "Timeout (seconds)" + } + } +} +``` + +### Embedding with Dimensions + +```json +{ + "title": "OpenAI Embedding", + "type": "object", + "required": ["adapter_name", "api_key"], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name" + }, + "model": { + "type": "string", + "title": "Model", + "default": "text-embedding-3-small", + "description": "text-embedding-3-small/large support custom dimensions" + }, + "api_key": { + "type": "string", + "format": "password", + "title": "API Key" + }, + "dimensions": { + "type": "number", + "minimum": 1, + "multipleOf": 1, + "title": "Dimensions", + "description": "Output dimensions (only for text-embedding-3-* models)" + } + } +} +``` + +### Reasoning with Effort Control (Mistral Magistral, OpenAI o1/o3) + +```json +{ + "properties": { + "enable_reasoning": { + "type": "boolean", + "title": "Enable Reasoning", + "default": false, + "description": "Enable reasoning for Magistral models" + } + }, + "allOf": [ + { + "if": { + "properties": { "enable_reasoning": { "const": true } } + }, + "then": { + "properties": { + "reasoning_effort": { + "type": "string", + "enum": ["low", "medium", "high"], + "default": "medium", + "title": "Reasoning Effort" + } + }, + "required": ["reasoning_effort"] + } + } + ] +} +``` + +### Optional Credentials (AWS Bedrock) + +```json +{ + "title": "Bedrock LLM", + "type": "object", + "required": ["adapter_name", "region_name", "model"], + "properties": { + "adapter_name": { "type": "string", "title": "Name" }, + "model": { "type": "string", "title": "Model" }, + "region_name": { "type": "string", "title": "AWS Region" }, + "aws_access_key_id": { + "type": "string", + "format": "password", + "title": "AWS Access Key ID", + "description": "Leave empty if using AWS Profile or IAM role." + }, + "aws_secret_access_key": { + "type": "string", + "format": "password", + "title": "AWS Secret Access Key", + "description": "Leave empty if using AWS Profile or IAM role." + }, + "aws_profile_name": { + "type": "string", + "title": "AWS Profile Name", + "description": "AWS SSO profile name for authentication." + } + } +} +``` + +### JSON Mode Toggle (Ollama) + +```json +{ + "properties": { + "json_mode": { + "type": "boolean", + "title": "JSON Mode", + "default": false, + "description": "Constrain output to valid JSON" + } + } +} +``` + +## Best Practices + +1. **Always include `adapter_name`** as required field +2. **Use `format: "password"`** for secrets and API keys +3. **Provide sensible defaults** for optional fields +4. **Add descriptions** for non-obvious fields +5. **Use enums** when choices are limited +6. **Keep titles short** - they become form labels +7. **Order properties** by importance/usage frequency +8. **Use conditional fields** to reduce clutter +9. **Validate with JSON Schema validator** before deploying +10. **Make credentials optional** when multiple auth methods exist diff --git a/.claude/skills/unstract-adapter-extension/references/provider_capabilities.md b/.claude/skills/unstract-adapter-extension/references/provider_capabilities.md new file mode 100644 index 0000000000..cebb66489d --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/references/provider_capabilities.md @@ -0,0 +1,145 @@ +# Provider Capabilities Reference + +Quick reference for LLM and embedding provider features supported via LiteLLM. + +## LLM Provider Features + +| Provider | Reasoning | Thinking | JSON Mode | Tools | Streaming | +|----------|:---------:|:--------:|:---------:|:-----:|:---------:| +| OpenAI | ✅ o1/o3 | ❌ | ✅ | ✅ | ✅ | +| Anthropic | ❌ | ✅ Claude 3.7+ | ✅ | ✅ | ✅ | +| Azure OpenAI | ✅ o1/o3 | ❌ | ✅ | ✅ | ✅ | +| AWS Bedrock | ❌ | ✅ Claude 3.7+ | ✅ | ✅ | ✅ | +| VertexAI | ✅ Gemini 2.5 | ✅ Gemini 2.5 | ✅ | ✅ | ✅ | +| Mistral | ✅ Magistral | ❌ | ✅ | ✅ | ✅ | +| Ollama | ❌ | ❌ | ✅ | ✅ | ✅ | +| Anyscale | ❌ | ❌ | ❌ | ❌ | ✅ | + +### Feature Definitions + +- **Reasoning**: `enable_reasoning` + `reasoning_effort` (low/medium/high) for chain-of-thought +- **Thinking**: `enable_thinking` + `budget_tokens` for extended internal reasoning +- **JSON Mode**: `response_format` or `json_mode` for structured output +- **Tools**: Function calling / tool use capability +- **Streaming**: Token-by-token response streaming + +## Embedding Provider Features + +| Provider | Dimensions | Batch Size | Model Prefix | +|----------|:----------:|:----------:|:------------:| +| OpenAI | ✅ v3 only | ✅ | ❌ | +| Azure OpenAI | ✅ v3 only | ✅ | ❌ | +| AWS Bedrock | ❌ | ✅ | `bedrock/` | +| VertexAI | ✅ | ✅ | `vertex_ai/` | +| Ollama | ❌ | ✅ | ❌ | + +### Feature Definitions + +- **Dimensions**: Custom output embedding dimensions (only text-embedding-3-* models) +- **Batch Size**: `embed_batch_size` for controlling request batching +- **Model Prefix**: Whether LiteLLM requires provider prefix on model name + +## Provider-Specific Parameters + +### OpenAI LLM +``` +api_key, api_base, model, max_tokens, temperature, top_p +enable_reasoning, reasoning_effort (o1/o3 models) +``` + +### Anthropic LLM +``` +api_key, model, max_tokens, temperature +enable_thinking, budget_tokens (Claude 3.7+) +``` + +### Azure OpenAI LLM +``` +api_key, azure_endpoint, api_version, deployment_name, model +max_tokens, temperature, enable_reasoning, reasoning_effort +``` + +### AWS Bedrock LLM +``` +aws_access_key_id, aws_secret_access_key, region_name +aws_profile_name (SSO), model_id (inference profile ARN) +model, max_tokens, enable_thinking, budget_tokens +``` + +### VertexAI LLM +``` +json_credentials, project, model, max_tokens, temperature +enable_thinking, budget_tokens, reasoning_effort (Gemini 2.5) +``` + +### Mistral LLM +``` +api_key, model, max_tokens, max_retries, timeout +enable_reasoning, reasoning_effort (Magistral models) +``` + +### Ollama LLM +``` +base_url, model, max_tokens, temperature, context_window +request_timeout, json_mode +``` + +### Anyscale LLM +``` +api_key, api_base, model, max_tokens, max_retries, timeout +``` + +## Authentication Methods + +| Provider | API Key | OAuth | IAM Role | SSO Profile | +|----------|:-------:|:-----:|:--------:|:-----------:| +| OpenAI | ✅ | ❌ | ❌ | ❌ | +| Anthropic | ✅ | ❌ | ❌ | ❌ | +| Azure | ✅ | ✅ | ✅ | ❌ | +| AWS Bedrock | ✅ | ❌ | ✅ | ✅ | +| VertexAI | JSON | ❌ | ✅ | ❌ | +| Mistral | ✅ | ❌ | ❌ | ❌ | +| Ollama | ❌ | ❌ | ❌ | ❌ | + +## LiteLLM Model Prefixes + +| Provider | LLM Prefix | Embedding Prefix | +|----------|------------|------------------| +| OpenAI | `openai/` | (none) | +| Anthropic | `anthropic/` | N/A | +| Azure | `azure/` | `azure/` | +| AWS Bedrock | `bedrock/` | `bedrock/` | +| VertexAI | `vertex_ai/` | `vertex_ai/` | +| Mistral | `mistral/` | N/A | +| Ollama | `ollama_chat/` | `ollama/` | +| Anyscale | `anyscale/` | N/A | + +## Models Supporting Advanced Features + +### Reasoning Models (reasoning_effort) +- OpenAI: `o1-mini`, `o1-preview`, `o3-mini`, `o3`, `o4-mini` +- Azure: `o1-mini`, `o1-preview` (via deployment) +- Mistral: `magistral-medium-2506`, `magistral-small-2506` +- VertexAI: `gemini-2.5-flash-preview`, `gemini-2.5-pro` + +### Thinking Models (budget_tokens) +- Anthropic: `claude-3-7-sonnet`, `claude-sonnet-4`, `claude-opus-4` +- Bedrock: `anthropic.claude-3-7-sonnet-*` +- VertexAI: `gemini-2.5-flash-preview`, `gemini-2.5-pro` + +### Embedding Models with Dimensions +- OpenAI: `text-embedding-3-small`, `text-embedding-3-large` +- Azure: `text-embedding-3-small`, `text-embedding-3-large` (via deployment) + +## Documentation Links + +| Provider | LiteLLM Docs | +|----------|--------------| +| OpenAI | https://docs.litellm.ai/docs/providers/openai | +| Anthropic | https://docs.litellm.ai/docs/providers/anthropic | +| Azure | https://docs.litellm.ai/docs/providers/azure | +| Bedrock | https://docs.litellm.ai/docs/providers/bedrock | +| VertexAI | https://docs.litellm.ai/docs/providers/vertex | +| Mistral | https://docs.litellm.ai/docs/providers/mistral | +| Ollama | https://docs.litellm.ai/docs/providers/ollama | +| Embedding | https://docs.litellm.ai/docs/embedding/supported_embedding | diff --git a/.claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py b/.claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py new file mode 100644 index 0000000000..f09fb20cbb --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 +"""Check for updates to existing LLM and Embedding adapters. + +This script analyzes current adapter JSON schemas and compares them +against known LiteLLM features to identify potential updates. + +Usage: + python check_adapter_updates.py + python check_adapter_updates.py --adapter llm + python check_adapter_updates.py --adapter embedding + python check_adapter_updates.py --provider openai +""" + +import argparse +import json +import sys +from pathlib import Path + +# Resolve paths +SCRIPT_DIR = Path(__file__).parent +SKILL_DIR = SCRIPT_DIR.parent +REPO_ROOT = SKILL_DIR.parent.parent.parent +SDK1_ADAPTERS = REPO_ROOT / "unstract" / "sdk1" / "src" / "unstract" / "sdk1" / "adapters" + +# Known LiteLLM features by provider (update this periodically) +LITELLM_FEATURES = { + "llm": { + "openai": { + "known_params": [ + "api_key", "api_base", "api_version", "model", "max_tokens", + "max_retries", "timeout", "temperature", "top_p", "n", + "enable_reasoning", "reasoning_effort", "seed", "response_format", + "tools", "tool_choice", "parallel_tool_calls", "logprobs" + ], + "reasoning_models": ["o1-mini", "o1-preview", "o3-mini", "o3", "o4-mini"], + "latest_models": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-5"], + "docs_url": "https://docs.litellm.ai/docs/providers/openai" + }, + "anthropic": { + "known_params": [ + "api_key", "model", "max_tokens", "max_retries", "timeout", + "temperature", "enable_thinking", "budget_tokens", "thinking" + ], + "thinking_models": ["claude-3-7-sonnet", "claude-sonnet-4", "claude-opus-4"], + "latest_models": ["claude-sonnet-4-5-20250929", "claude-opus-4-1-20250805"], + "docs_url": "https://docs.litellm.ai/docs/providers/anthropic" + }, + "azure": { + "known_params": [ + "api_key", "api_base", "api_version", "deployment_name", "azure_endpoint", + "model", "max_tokens", "max_retries", "timeout", "temperature", + "enable_reasoning", "reasoning_effort" + ], + "reasoning_models": ["o1-mini", "o1-preview"], + "docs_url": "https://docs.litellm.ai/docs/providers/azure" + }, + "bedrock": { + "known_params": [ + "aws_access_key_id", "aws_secret_access_key", "region_name", + "aws_region_name", "aws_profile_name", "model_id", "model", + "max_tokens", "max_retries", "timeout", "temperature", + "enable_thinking", "budget_tokens", "thinking", "top_k" + ], + "thinking_models": ["anthropic.claude-3-7-sonnet"], + "docs_url": "https://docs.litellm.ai/docs/providers/bedrock" + }, + "vertex_ai": { + "known_params": [ + "json_credentials", "vertex_credentials", "project", "vertex_project", + "model", "max_tokens", "max_retries", "timeout", "temperature", + "safety_settings", "enable_thinking", "budget_tokens", "thinking", + "reasoning_effort", "tools", "googleSearch" + ], + "thinking_models": ["gemini-2.5-flash-preview", "gemini-2.5-pro"], + "docs_url": "https://docs.litellm.ai/docs/providers/vertex" + }, + "mistral": { + "known_params": [ + "api_key", "model", "max_tokens", "max_retries", "timeout", + "temperature", "enable_reasoning", "reasoning_effort", "tools" + ], + "reasoning_models": ["magistral-medium-2506", "magistral-small-2506"], + "latest_models": ["mistral-large-latest", "mistral-small-latest"], + "docs_url": "https://docs.litellm.ai/docs/providers/mistral" + }, + "ollama": { + "known_params": [ + "base_url", "api_base", "model", "max_tokens", "temperature", + "context_window", "request_timeout", "json_mode", "response_format", + "tools" + ], + "docs_url": "https://docs.litellm.ai/docs/providers/ollama" + }, + "anyscale": { + "known_params": [ + "api_key", "api_base", "model", "max_tokens", "max_retries", + "timeout", "temperature", "additional_kwargs" + ], + "docs_url": "https://docs.litellm.ai/docs/providers/anyscale" + } + }, + "embedding": { + "openai": { + "known_params": [ + "api_key", "api_base", "model", "embed_batch_size", "timeout", + "dimensions" + ], + "latest_models": ["text-embedding-3-small", "text-embedding-3-large"], + "docs_url": "https://docs.litellm.ai/docs/embedding/supported_embedding" + }, + "azure": { + "known_params": [ + "api_key", "api_base", "api_version", "deployment_name", + "azure_endpoint", "model", "embed_batch_size", "timeout", + "dimensions" + ], + "docs_url": "https://docs.litellm.ai/docs/providers/azure" + }, + "bedrock": { + "known_params": [ + "aws_access_key_id", "aws_secret_access_key", "region_name", + "aws_region_name", "model", "max_retries", "timeout" + ], + "docs_url": "https://docs.litellm.ai/docs/providers/bedrock_embedding" + }, + "vertexai": { + "known_params": [ + "json_credentials", "vertex_credentials", "project", "vertex_project", + "model", "embed_batch_size", "embed_mode", "dimensions", "input_type" + ], + "docs_url": "https://docs.litellm.ai/docs/providers/vertex" + }, + "ollama": { + "known_params": [ + "base_url", "api_base", "model_name", "model", "embed_batch_size" + ], + "docs_url": "https://docs.litellm.ai/docs/providers/ollama" + } + } +} + + +def load_json_schema(adapter_type: str, provider: str) -> dict | None: + """Load JSON schema for an adapter.""" + schema_dir = SDK1_ADAPTERS / f"{adapter_type}1" / "static" + + # Try common filename patterns + for filename in [f"{provider}.json", f"{provider.replace('_', '')}.json"]: + schema_path = schema_dir / filename + if schema_path.exists(): + with open(schema_path) as f: + return json.load(f) + + return None + + +def get_schema_properties(schema: dict) -> set[str]: + """Extract property names from a JSON schema.""" + properties = set() + + if "properties" in schema: + properties.update(schema["properties"].keys()) + + # Check allOf conditional properties + if "allOf" in schema: + for item in schema["allOf"]: + if "then" in item and "properties" in item["then"]: + properties.update(item["then"]["properties"].keys()) + + return properties + + +def analyze_adapter(adapter_type: str, provider: str) -> dict: + """Analyze a single adapter for potential updates.""" + result = { + "provider": provider, + "adapter_type": adapter_type, + "status": "ok", + "current_properties": [], + "missing_properties": [], + "suggestions": [], + "docs_url": None + } + + # Load schema + schema = load_json_schema(adapter_type, provider) + if not schema: + result["status"] = "error" + result["suggestions"].append(f"Schema not found for {provider}") + return result + + # Get current properties + current_props = get_schema_properties(schema) + result["current_properties"] = sorted(current_props) + + # Get known LiteLLM features + features = LITELLM_FEATURES.get(adapter_type, {}).get(provider, {}) + if not features: + result["suggestions"].append(f"No LiteLLM feature data for {provider}") + return result + + result["docs_url"] = features.get("docs_url") + + # Find missing parameters + known_params = set(features.get("known_params", [])) + missing = known_params - current_props - {"adapter_name"} # adapter_name is always present + + # Filter out params that might be named differently + common_aliases = { + "api_base": "base_url", + "base_url": "api_base", + "vertex_credentials": "json_credentials", + "vertex_project": "project", + "aws_region_name": "region_name" + } + + filtered_missing = set() + for param in missing: + alias = common_aliases.get(param) + if alias and alias in current_props: + continue + filtered_missing.add(param) + + result["missing_properties"] = sorted(filtered_missing) + + # Generate suggestions + if filtered_missing: + result["status"] = "needs_update" + result["suggestions"].append( + f"Consider adding: {', '.join(sorted(filtered_missing))}" + ) + + # Check for reasoning/thinking support + if adapter_type == "llm": + reasoning_models = features.get("reasoning_models", []) + thinking_models = features.get("thinking_models", []) + + if reasoning_models and "enable_reasoning" not in current_props and "reasoning_effort" not in current_props: + result["suggestions"].append( + f"Consider adding reasoning support for models: {', '.join(reasoning_models)}" + ) + + if thinking_models and "enable_thinking" not in current_props: + result["suggestions"].append( + f"Consider adding thinking support for models: {', '.join(thinking_models)}" + ) + + # Check for model updates + if "model" in schema.get("properties", {}): + model_prop = schema["properties"]["model"] + default_model = model_prop.get("default", "") + latest_models = features.get("latest_models", []) + + if latest_models and default_model and default_model not in latest_models: + result["suggestions"].append( + f"Default model '{default_model}' may be outdated. Latest: {', '.join(latest_models[:3])}" + ) + + return result + + +def list_adapters(adapter_type: str) -> list[str]: + """List all adapters of a given type.""" + adapter_dir = SDK1_ADAPTERS / f"{adapter_type}1" + if not adapter_dir.exists(): + return [] + + adapters = [] + for py_file in adapter_dir.glob("*.py"): + if py_file.name.startswith("__"): + continue + adapters.append(py_file.stem) + + return adapters + + +def print_report(results: list[dict]) -> None: + """Print analysis report.""" + print("\n" + "=" * 70) + print("ADAPTER UPDATE CHECK REPORT") + print("=" * 70) + + needs_update = [r for r in results if r["status"] == "needs_update"] + ok = [r for r in results if r["status"] == "ok"] + errors = [r for r in results if r["status"] == "error"] + + if needs_update: + print(f"\n🟡 NEEDS UPDATE ({len(needs_update)}):") + print("-" * 40) + for r in needs_update: + print(f"\n {r['adapter_type'].upper()}: {r['provider']}") + if r["missing_properties"]: + print(f" Missing: {', '.join(r['missing_properties'])}") + for s in r["suggestions"]: + print(f" → {s}") + if r["docs_url"]: + print(f" Docs: {r['docs_url']}") + + if ok: + print(f"\n✅ UP TO DATE ({len(ok)}):") + print("-" * 40) + for r in ok: + print(f" {r['adapter_type'].upper()}: {r['provider']}") + + if errors: + print(f"\n❌ ERRORS ({len(errors)}):") + print("-" * 40) + for r in errors: + print(f" {r['adapter_type'].upper()}: {r['provider']}") + for s in r["suggestions"]: + print(f" → {s}") + + print("\n" + "=" * 70) + print(f"Total: {len(results)} adapters checked") + print(f" Needs update: {len(needs_update)}") + print(f" Up to date: {len(ok)}") + print(f" Errors: {len(errors)}") + print("=" * 70 + "\n") + + +def main(): + parser = argparse.ArgumentParser( + description="Check for updates to LLM and Embedding adapters" + ) + parser.add_argument( + "--adapter", + choices=["llm", "embedding", "all"], + default="all", + help="Type of adapter to check" + ) + parser.add_argument( + "--provider", + help="Specific provider to check (e.g., openai, anthropic)" + ) + parser.add_argument( + "--json", + action="store_true", + help="Output results as JSON" + ) + + args = parser.parse_args() + + print(f"SDK1 Adapters Path: {SDK1_ADAPTERS}") + + adapter_types = ["llm", "embedding"] if args.adapter == "all" else [args.adapter] + results = [] + + for adapter_type in adapter_types: + if args.provider: + providers = [args.provider] + else: + providers = list_adapters(adapter_type) + + for provider in providers: + result = analyze_adapter(adapter_type, provider) + results.append(result) + + if args.json: + print(json.dumps(results, indent=2)) + else: + print_report(results) + + # Return exit code based on results + needs_update = any(r["status"] == "needs_update" for r in results) + return 1 if needs_update else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py b/.claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py new file mode 100755 index 0000000000..c37344f7de --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py @@ -0,0 +1,613 @@ +#!/usr/bin/env python3 +"""Initialize a new Embedding adapter for unstract/sdk1. + +Usage: + python init_embedding_adapter.py --provider newprovider --name "New Provider" --description "Description" + python init_embedding_adapter.py --provider newprovider --name "New Provider" --description "Description" --logo-url "https://example.com/logo.png" + python init_embedding_adapter.py --provider newprovider --name "New Provider" --description "Description" --auto-logo + +This script creates: + 1. Adapter Python file in embedding1/{provider}.py + 2. JSON schema in embedding1/static/{provider}.json + 3. Optionally adds parameter class stub to base1.py + 4. Optionally downloads and adds provider logo (from URL or auto-detected) +""" + +import argparse +import json +import sys +import uuid +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError + +# Resolve paths +SCRIPT_DIR = Path(__file__).parent +SKILL_DIR = SCRIPT_DIR.parent +# Find the sdk1 adapters directory relative to the repo root +REPO_ROOT = SKILL_DIR.parent.parent.parent # .claude/skills/unstract-adapter-extension -> repo root +SDK1_ADAPTERS = REPO_ROOT / "unstract" / "sdk1" / "src" / "unstract" / "sdk1" / "adapters" +ICONS_DIR = REPO_ROOT / "frontend" / "public" / "icons" / "adapter-icons" + +EMBEDDING_ADAPTER_TEMPLATE = '''from typing import Any + +from unstract.sdk1.adapters.base1 import BaseAdapter, {param_class} +from unstract.sdk1.adapters.enums import AdapterTypes + + +class {class_name}({param_class}, BaseAdapter): + @staticmethod + def get_id() -> str: + return "{provider}|{uuid}" + + @staticmethod + def get_metadata() -> dict[str, Any]: + return {{ + "name": "{display_name}", + "version": "1.0.0", + "adapter": {class_name}, + "description": "{description}", + "is_active": True, + }} + + @staticmethod + def get_name() -> str: + return "{display_name}" + + @staticmethod + def get_description() -> str: + return "{description}" + + @staticmethod + def get_provider() -> str: + return "{provider}" + + @staticmethod + def get_icon() -> str: + return "/icons/adapter-icons/{icon_name}.png" + + @staticmethod + def get_adapter_type() -> AdapterTypes: + return AdapterTypes.EMBEDDING +''' + +EMBEDDING_SCHEMA_TEMPLATE = { + "title": "{display_name} Embedding", + "type": "object", + "required": ["adapter_name", "api_key"], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name", + "default": "", + "description": "Provide a unique name for this adapter instance. Example: {provider}-emb-1" + }, + "model": { + "type": "string", + "title": "Model", + "default": "", + "description": "Provide the name of the embedding model." + }, + "api_key": { + "type": "string", + "title": "API Key", + "default": "", + "format": "password", + "description": "Your {display_name} API key." + }, + "api_base": { + "type": "string", + "title": "API Base", + "format": "uri", + "default": "", + "description": "API endpoint URL (if different from default)." + }, + "embed_batch_size": { + "type": "number", + "minimum": 1, + "multipleOf": 1, + "title": "Embed Batch Size", + "default": 10, + "description": "Number of texts to embed in each batch." + }, + "timeout": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Timeout", + "default": 240, + "description": "Timeout in seconds" + } + } +} + +PARAMETER_CLASS_TEMPLATE = ''' +class {param_class}(BaseEmbeddingParameters): + """See https://docs.litellm.ai/docs/providers/{provider}.""" + + api_key: str + api_base: str | None = None + embed_batch_size: int | None = 10 + + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + adapter_metadata["model"] = {param_class}.validate_model(adapter_metadata) + return {param_class}(**adapter_metadata).model_dump() + + @staticmethod + def validate_model(adapter_metadata: dict[str, "Any"]) -> str: + model = adapter_metadata.get("model", "") + return model + +''' + + +def to_class_name(provider: str) -> str: + """Convert provider name to class name format.""" + special_cases = { + "openai": "OpenAI", + "azure_openai": "AzureOpenAI", + "azure_ai_foundry": "AzureAIFoundry", + "azure_ai": "AzureAI", + "vertexai": "VertexAI", + "aws_bedrock": "AWSBedrock", + "bedrock": "AWSBedrock", + } + if provider.lower() in special_cases: + return special_cases[provider.lower()] + + return "".join(word.capitalize() for word in provider.replace("_", " ").replace("-", " ").split()) + + +def to_icon_name(display_name: str) -> str: + """Convert display name to icon filename (without extension).""" + return display_name.replace(" ", "") + + +def fetch_url(url: str, timeout: int = 10) -> bytes | None: + """Fetch content from URL with error handling.""" + try: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + request = Request(url, headers=headers) + with urlopen(request, timeout=timeout) as response: + return response.read() + except (URLError, HTTPError, TimeoutError): + return None + + +def search_potential_logo_sources(provider: str, display_name: str) -> list[dict]: + """Search for potential logo sources for the given provider. + + This function only SEARCHES for potential sources and returns them. + It does NOT verify if the logos are correct - that's up to the user. + + Returns: + List of dicts with 'url' and 'source' keys for potential logos found + """ + found_sources = [] + provider_lower = provider.lower().replace("_", "").replace("-", "") + name_lower = display_name.lower().replace(" ", "") + + # Try Clearbit Logo API with common domain patterns + domains = [ + (f"{provider_lower}.com", "company domain"), + (f"{provider_lower}.ai", "AI domain"), + (f"{name_lower}.com", "name domain"), + (f"{name_lower}.ai", "name AI domain"), + ] + + for domain, source_type in domains: + url = f"https://logo.clearbit.com/{domain}" + try: + headers = {'User-Agent': 'Mozilla/5.0'} + request = Request(url, headers=headers, method='HEAD') + with urlopen(request, timeout=5) as response: + if response.status == 200: + found_sources.append({ + 'url': url, + 'source': f"Clearbit ({source_type}: {domain})" + }) + except (URLError, HTTPError, TimeoutError): + continue + + # Try GitHub organization avatars + github_names = [ + provider_lower, + name_lower, + provider.lower().replace("_", "-"), + ] + + for name in github_names: + url = f"https://github.com/{name}.png?size=512" + try: + headers = {'User-Agent': 'Mozilla/5.0'} + request = Request(url, headers=headers, method='HEAD') + with urlopen(request, timeout=5) as response: + if response.status == 200: + content_type = response.headers.get('Content-Type', '') + if 'image' in content_type: + found_sources.append({ + 'url': url, + 'source': f"GitHub avatar (@{name})" + }) + except (URLError, HTTPError, TimeoutError): + continue + + return found_sources + + +def download_and_process_logo(url: str, output_path: Path, target_size: int = 512) -> bool: + """Download logo from URL and process to standard format. + + Args: + url: URL to download the logo from + output_path: Path to save the logo + target_size: Target size for square logo (default 512x512) + + Returns: + True if successful, False otherwise + + Image settings: 4800 DPI density, 8-bit depth, 512x512 pixels + """ + image_data = fetch_url(url, timeout=30) + if not image_data: + return False + + # Check if SVG (by URL extension or content) + is_svg = url.lower().endswith('.svg') or image_data[:5] == b' bool: + """Copy and optionally resize a local logo file. + + Image settings: 4800 DPI density, 8-bit depth, 512x512 pixels + """ + if not source_path.exists(): + return False + + # Check if SVG + is_svg = source_path.suffix.lower() == '.svg' + + if is_svg: + # Use ImageMagick for SVG conversion with optimal settings + import subprocess + + try: + result = subprocess.run( + ['magick', '-density', '4800', '-background', 'none', + str(source_path), '-resize', f'{target_size}x{target_size}', + '-depth', '8', str(output_path)], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f" ImageMagick error: {result.stderr}") + return False + return True + except FileNotFoundError: + print(" Note: ImageMagick not found. Install with: sudo pacman -S imagemagick") + return False + + # Handle raster images with PIL + try: + from PIL import Image + + img = Image.open(source_path) + if img.mode != 'RGBA': + img = img.convert('RGBA') + + if img.width != target_size or img.height != target_size: + ratio = min(target_size / img.width, target_size / img.height) + new_size = (int(img.width * ratio), int(img.height * ratio)) + img = img.resize(new_size, Image.Resampling.LANCZOS) + + canvas = Image.new('RGBA', (target_size, target_size), (255, 255, 255, 0)) + offset = ((target_size - img.width) // 2, (target_size - img.height) // 2) + canvas.paste(img, offset, img if img.mode == 'RGBA' else None) + img = canvas + + img.save(output_path, 'PNG') + return True + + except ImportError: + import shutil + shutil.copy2(source_path, output_path) + return True + except Exception: + return False + + +def create_embedding_adapter( + provider: str, + display_name: str, + description: str, + add_param_class: bool = False, + use_existing_param_class: str | None = None, + logo_url: str | None = None, + logo_file: str | None = None, + auto_logo: bool = False, +) -> dict: + """Create a new Embedding adapter. + + Returns: + dict with 'files_created' list and 'param_class_stub' if applicable + """ + result = {"files_created": [], "param_class_stub": None, "errors": [], "warnings": []} + + if not SDK1_ADAPTERS.exists(): + result["errors"].append(f"SDK1 adapters directory not found at: {SDK1_ADAPTERS}") + return result + + embedding_dir = SDK1_ADAPTERS / "embedding1" + static_dir = embedding_dir / "static" + + if not embedding_dir.exists(): + result["errors"].append(f"Embedding adapters directory not found at: {embedding_dir}") + return result + + static_dir.mkdir(exist_ok=True) + + class_base = to_class_name(provider) + class_name = f"{class_base}EmbeddingAdapter" + icon_name = to_icon_name(display_name) + adapter_uuid = str(uuid.uuid4()) + + if use_existing_param_class: + param_class = use_existing_param_class + elif add_param_class: + param_class = f"{class_base}EmbeddingParameters" + else: + param_class = "OpenAIEmbeddingParameters" + + adapter_content = EMBEDDING_ADAPTER_TEMPLATE.format( + class_name=class_name, + param_class=param_class, + provider=provider.lower(), + uuid=adapter_uuid, + display_name=display_name, + description=description, + icon_name=icon_name, + ) + + adapter_file = embedding_dir / f"{provider.lower()}.py" + if adapter_file.exists(): + result["errors"].append(f"Adapter file already exists: {adapter_file}") + else: + adapter_file.write_text(adapter_content) + result["files_created"].append(str(adapter_file)) + + schema = json.loads(json.dumps(EMBEDDING_SCHEMA_TEMPLATE)) + schema["title"] = f"{display_name} Embedding" + schema["properties"]["adapter_name"]["description"] = ( + f"Provide a unique name for this adapter instance. Example: {provider.lower()}-emb-1" + ) + schema["properties"]["api_key"]["description"] = f"Your {display_name} API key." + + schema_file = static_dir / f"{provider.lower()}.json" + if schema_file.exists(): + result["errors"].append(f"Schema file already exists: {schema_file}") + else: + schema_file.write_text(json.dumps(schema, indent=2) + "\n") + result["files_created"].append(str(schema_file)) + + # Handle logo + logo_path = ICONS_DIR / f"{icon_name}.png" + ICONS_DIR.mkdir(parents=True, exist_ok=True) + + if logo_path.exists(): + result["warnings"].append(f"Logo already exists: {logo_path}") + elif logo_url: + # User provided explicit URL - download it + if download_and_process_logo(logo_url, logo_path): + result["files_created"].append(str(logo_path)) + else: + result["warnings"].append(f"Failed to download logo from: {logo_url}") + elif logo_file: + # User provided local file - copy it + if copy_logo(Path(logo_file), logo_path): + result["files_created"].append(str(logo_path)) + else: + result["warnings"].append(f"Failed to copy logo from: {logo_file}") + elif auto_logo: + # Search for potential sources but DO NOT auto-download + # Just inform the user about what was found + print(f" Searching for potential logo sources for '{display_name}'...") + potential_sources = search_potential_logo_sources(provider, display_name) + if potential_sources: + result["logo_suggestions"] = potential_sources + else: + result["warnings"].append( + f"Could not find logo for '{display_name}'. " + f"Please add manually to: {logo_path}" + ) + + if add_param_class: + result["param_class_stub"] = PARAMETER_CLASS_TEMPLATE.format( + param_class=param_class, + provider=provider.lower(), + ) + + return result + + +def main(): + parser = argparse.ArgumentParser( + description="Initialize a new Embedding adapter for unstract/sdk1" + ) + parser.add_argument( + "--provider", + required=True, + help="Provider identifier (lowercase, e.g., 'newprovider')" + ) + parser.add_argument( + "--name", + required=True, + help="Display name for the provider (e.g., 'New Provider')" + ) + parser.add_argument( + "--description", + required=True, + help="Description of the adapter" + ) + parser.add_argument( + "--add-param-class", + action="store_true", + help="Generate a parameter class stub for base1.py" + ) + parser.add_argument( + "--use-param-class", + help="Use an existing parameter class (e.g., 'OpenAIEmbeddingParameters')" + ) + parser.add_argument( + "--logo-url", + help="URL to download the provider logo from" + ) + parser.add_argument( + "--logo-file", + help="Path to a local logo file to copy" + ) + parser.add_argument( + "--auto-logo", + action="store_true", + help="Automatically search for and download provider logo" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be created without actually creating files" + ) + + args = parser.parse_args() + + icon_name = to_icon_name(args.name) + + print(f"Creating Embedding adapter for: {args.name}") + print(f"Provider: {args.provider}") + print(f"SDK1 Path: {SDK1_ADAPTERS}") + print() + + if args.dry_run: + print("[DRY RUN] Would create:") + print(f" - {SDK1_ADAPTERS}/embedding1/{args.provider.lower()}.py") + print(f" - {SDK1_ADAPTERS}/embedding1/static/{args.provider.lower()}.json") + if args.logo_url or args.logo_file or args.auto_logo: + print(f" - {ICONS_DIR}/{icon_name}.png (if logo found)") + if args.add_param_class: + print(f" - Parameter class stub for base1.py") + return 0 + + result = create_embedding_adapter( + provider=args.provider, + display_name=args.name, + description=args.description, + add_param_class=args.add_param_class, + use_existing_param_class=args.use_param_class, + logo_url=args.logo_url, + logo_file=args.logo_file, + auto_logo=args.auto_logo, + ) + + if result["errors"]: + print("Errors:") + for error in result["errors"]: + print(f" - {error}") + return 1 + + if result["warnings"]: + print("Warnings:") + for warning in result["warnings"]: + print(f" - {warning}") + + print("Files created:") + for file in result["files_created"]: + print(f" - {file}") + + # Show logo suggestions if any were found + if result.get("logo_suggestions"): + print("\nPotential logo sources found (please verify before using):") + for i, suggestion in enumerate(result["logo_suggestions"], 1): + print(f" {i}. {suggestion['source']}") + print(f" URL: {suggestion['url']}") + print(f"\nTo use a logo, re-run with: --logo-url ") + print(f"Logo will be saved to: {ICONS_DIR}/{icon_name}.png") + + if result["param_class_stub"]: + print("\nParameter class stub (add to base1.py):") + print("-" * 60) + print(result["param_class_stub"]) + print("-" * 60) + + print("\nNext steps:") + print("1. Customize the JSON schema in static/{provider}.json") + print("2. If needed, add parameter class to base1.py") + print("3. Update the adapter to use the correct parameter class") + if not any("png" in f for f in result["files_created"]) and not result.get("logo_suggestions"): + print(f"4. Add logo manually to: {ICONS_DIR}/{icon_name}.png") + print("5. Test with: from unstract.sdk1.adapters.adapterkit import Adapterkit") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py b/.claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py new file mode 100755 index 0000000000..956adb1701 --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py @@ -0,0 +1,626 @@ +#!/usr/bin/env python3 +"""Initialize a new LLM adapter for unstract/sdk1. + +Usage: + python init_llm_adapter.py --provider newprovider --name "New Provider" --description "Description" + python init_llm_adapter.py --provider newprovider --name "New Provider" --description "Description" --logo-url "https://example.com/logo.png" + python init_llm_adapter.py --provider newprovider --name "New Provider" --description "Description" --auto-logo + +This script creates: + 1. Adapter Python file in llm1/{provider}.py + 2. JSON schema in llm1/static/{provider}.json + 3. Optionally adds parameter class stub to base1.py + 4. Optionally downloads and adds provider logo (from URL or auto-detected) +""" + +import argparse +import json +import re +import sys +import uuid +from pathlib import Path +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError + +# Resolve paths +SCRIPT_DIR = Path(__file__).parent +SKILL_DIR = SCRIPT_DIR.parent +# Find the sdk1 adapters directory relative to the repo root +REPO_ROOT = SKILL_DIR.parent.parent.parent # .claude/skills/unstract-adapter-extension -> repo root +SDK1_ADAPTERS = REPO_ROOT / "unstract" / "sdk1" / "src" / "unstract" / "sdk1" / "adapters" +ICONS_DIR = REPO_ROOT / "frontend" / "public" / "icons" / "adapter-icons" + +LLM_ADAPTER_TEMPLATE = '''from typing import Any + +from unstract.sdk1.adapters.base1 import BaseAdapter, {param_class} +from unstract.sdk1.adapters.enums import AdapterTypes + + +class {class_name}({param_class}, BaseAdapter): + @staticmethod + def get_id() -> str: + return "{provider}|{uuid}" + + @staticmethod + def get_metadata() -> dict[str, Any]: + return {{ + "name": "{display_name}", + "version": "1.0.0", + "adapter": {class_name}, + "description": "{description}", + "is_active": True, + }} + + @staticmethod + def get_name() -> str: + return "{display_name}" + + @staticmethod + def get_description() -> str: + return "{description}" + + @staticmethod + def get_provider() -> str: + return "{provider}" + + @staticmethod + def get_icon() -> str: + return "/icons/adapter-icons/{icon_name}.png" + + @staticmethod + def get_adapter_type() -> AdapterTypes: + return AdapterTypes.LLM +''' + +LLM_SCHEMA_TEMPLATE = { + "title": "{display_name} LLM", + "type": "object", + "required": ["adapter_name", "api_key"], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name", + "default": "", + "description": "Provide a unique name for this adapter instance. Example: {provider}-llm-1" + }, + "api_key": { + "type": "string", + "title": "API Key", + "format": "password", + "description": "Your {display_name} API key." + }, + "model": { + "type": "string", + "title": "Model", + "default": "", + "description": "The model to use for the API request." + }, + "max_tokens": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Maximum Output Tokens", + "description": "Maximum number of output tokens to limit LLM replies." + }, + "max_retries": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Max Retries", + "default": 5, + "description": "The maximum number of times to retry a request if it fails." + }, + "timeout": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Timeout", + "default": 900, + "description": "Timeout in seconds" + } + } +} + +PARAMETER_CLASS_TEMPLATE = ''' +class {param_class}(BaseChatCompletionParameters): + """See https://docs.litellm.ai/docs/providers/{provider}.""" + + api_key: str + + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + adapter_metadata["model"] = {param_class}.validate_model(adapter_metadata) + return {param_class}(**adapter_metadata).model_dump() + + @staticmethod + def validate_model(adapter_metadata: dict[str, "Any"]) -> str: + model = adapter_metadata.get("model", "") + # Only add {provider}/ prefix if the model doesn't already have it + if model.startswith("{provider}/"): + return model + else: + return f"{provider}/{{model}}" + +''' + + +def to_class_name(provider: str) -> str: + """Convert provider name to class name format.""" + # Handle special cases + special_cases = { + "openai": "OpenAI", + "azure_openai": "AzureOpenAI", + "azure_ai_foundry": "AzureAIFoundry", + "azure_ai": "AzureAI", + "vertexai": "VertexAI", + "aws_bedrock": "AWSBedrock", + "bedrock": "AWSBedrock", + } + if provider.lower() in special_cases: + return special_cases[provider.lower()] + + # Default: capitalize each word + return "".join(word.capitalize() for word in provider.replace("_", " ").replace("-", " ").split()) + + +def to_icon_name(display_name: str) -> str: + """Convert display name to icon filename (without extension). + + Removes spaces and special characters for cleaner filenames. + """ + return display_name.replace(" ", "") + + +def fetch_url(url: str, timeout: int = 10) -> bytes | None: + """Fetch content from URL with error handling.""" + try: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + request = Request(url, headers=headers) + with urlopen(request, timeout=timeout) as response: + return response.read() + except (URLError, HTTPError, TimeoutError): + return None + + +def search_potential_logo_sources(provider: str, display_name: str) -> list[dict]: + """Search for potential logo sources for the given provider. + + This function only SEARCHES for potential sources and returns them. + It does NOT verify if the logos are correct - that's up to the user. + + Returns: + List of dicts with 'url' and 'source' keys for potential logos found + """ + found_sources = [] + provider_lower = provider.lower().replace("_", "").replace("-", "") + name_lower = display_name.lower().replace(" ", "") + + # Try Clearbit Logo API with common domain patterns + domains = [ + (f"{provider_lower}.com", "company domain"), + (f"{provider_lower}.ai", "AI domain"), + (f"{name_lower}.com", "name domain"), + (f"{name_lower}.ai", "name AI domain"), + ] + + for domain, source_type in domains: + url = f"https://logo.clearbit.com/{domain}" + try: + headers = {'User-Agent': 'Mozilla/5.0'} + request = Request(url, headers=headers, method='HEAD') + with urlopen(request, timeout=5) as response: + if response.status == 200: + found_sources.append({ + 'url': url, + 'source': f"Clearbit ({source_type}: {domain})" + }) + except (URLError, HTTPError, TimeoutError): + continue + + # Try GitHub organization avatars + github_names = [ + provider_lower, + name_lower, + provider.lower().replace("_", "-"), + ] + + for name in github_names: + url = f"https://github.com/{name}.png?size=512" + try: + headers = {'User-Agent': 'Mozilla/5.0'} + request = Request(url, headers=headers, method='HEAD') + with urlopen(request, timeout=5) as response: + if response.status == 200: + content_type = response.headers.get('Content-Type', '') + if 'image' in content_type: + found_sources.append({ + 'url': url, + 'source': f"GitHub avatar (@{name})" + }) + except (URLError, HTTPError, TimeoutError): + continue + + return found_sources + + +def download_and_process_logo(url: str, output_path: Path, target_size: int = 512) -> bool: + """Download logo from URL and process to standard format. + + Args: + url: URL to download the logo from + output_path: Path to save the logo + target_size: Target size for square logo (default 512x512) + + Returns: + True if successful, False otherwise + + Image settings: 4800 DPI density, 8-bit depth, 512x512 pixels + """ + image_data = fetch_url(url, timeout=30) + if not image_data: + return False + + # Check if SVG (by URL extension or content) + is_svg = url.lower().endswith('.svg') or image_data[:5] == b' bool: + """Copy and optionally resize a local logo file. + + Image settings: 4800 DPI density, 8-bit depth, 512x512 pixels + """ + if not source_path.exists(): + return False + + # Check if SVG + is_svg = source_path.suffix.lower() == '.svg' + + if is_svg: + # Use ImageMagick for SVG conversion with optimal settings + import subprocess + + try: + result = subprocess.run( + ['magick', '-density', '4800', '-background', 'none', + str(source_path), '-resize', f'{target_size}x{target_size}', + '-depth', '8', str(output_path)], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f" ImageMagick error: {result.stderr}") + return False + return True + except FileNotFoundError: + print(" Note: ImageMagick not found. Install with: sudo pacman -S imagemagick") + return False + + # Handle raster images with PIL + try: + from PIL import Image + + img = Image.open(source_path) + if img.mode != 'RGBA': + img = img.convert('RGBA') + + if img.width != target_size or img.height != target_size: + ratio = min(target_size / img.width, target_size / img.height) + new_size = (int(img.width * ratio), int(img.height * ratio)) + img = img.resize(new_size, Image.Resampling.LANCZOS) + + canvas = Image.new('RGBA', (target_size, target_size), (255, 255, 255, 0)) + offset = ((target_size - img.width) // 2, (target_size - img.height) // 2) + canvas.paste(img, offset, img if img.mode == 'RGBA' else None) + img = canvas + + img.save(output_path, 'PNG') + return True + + except ImportError: + import shutil + shutil.copy2(source_path, output_path) + return True + except Exception: + return False + + +def create_llm_adapter( + provider: str, + display_name: str, + description: str, + add_param_class: bool = False, + use_existing_param_class: str | None = None, + logo_url: str | None = None, + logo_file: str | None = None, + auto_logo: bool = False, +) -> dict: + """Create a new LLM adapter. + + Returns: + dict with 'files_created' list and 'param_class_stub' if applicable + """ + result = {"files_created": [], "param_class_stub": None, "errors": [], "warnings": []} + + # Validate SDK1 path exists + if not SDK1_ADAPTERS.exists(): + result["errors"].append(f"SDK1 adapters directory not found at: {SDK1_ADAPTERS}") + return result + + llm_dir = SDK1_ADAPTERS / "llm1" + static_dir = llm_dir / "static" + + if not llm_dir.exists(): + result["errors"].append(f"LLM adapters directory not found at: {llm_dir}") + return result + + static_dir.mkdir(exist_ok=True) + + # Generate identifiers + class_base = to_class_name(provider) + class_name = f"{class_base}LLMAdapter" + icon_name = to_icon_name(display_name) + adapter_uuid = str(uuid.uuid4()) + + # Determine parameter class + if use_existing_param_class: + param_class = use_existing_param_class + elif add_param_class: + param_class = f"{class_base}LLMParameters" + else: + param_class = "AnyscaleLLMParameters" + + # Create adapter file + adapter_content = LLM_ADAPTER_TEMPLATE.format( + class_name=class_name, + param_class=param_class, + provider=provider.lower(), + uuid=adapter_uuid, + display_name=display_name, + description=description, + icon_name=icon_name, + ) + + adapter_file = llm_dir / f"{provider.lower()}.py" + if adapter_file.exists(): + result["errors"].append(f"Adapter file already exists: {adapter_file}") + else: + adapter_file.write_text(adapter_content) + result["files_created"].append(str(adapter_file)) + + # Create JSON schema + schema = json.loads(json.dumps(LLM_SCHEMA_TEMPLATE)) + schema["title"] = f"{display_name} LLM" + schema["properties"]["adapter_name"]["description"] = ( + f"Provide a unique name for this adapter instance. Example: {provider.lower()}-llm-1" + ) + schema["properties"]["api_key"]["description"] = f"Your {display_name} API key." + + schema_file = static_dir / f"{provider.lower()}.json" + if schema_file.exists(): + result["errors"].append(f"Schema file already exists: {schema_file}") + else: + schema_file.write_text(json.dumps(schema, indent=2) + "\n") + result["files_created"].append(str(schema_file)) + + # Handle logo + logo_path = ICONS_DIR / f"{icon_name}.png" + ICONS_DIR.mkdir(parents=True, exist_ok=True) + + if logo_path.exists(): + result["warnings"].append(f"Logo already exists: {logo_path}") + elif logo_url: + # User provided explicit URL - download it + if download_and_process_logo(logo_url, logo_path): + result["files_created"].append(str(logo_path)) + else: + result["warnings"].append(f"Failed to download logo from: {logo_url}") + elif logo_file: + # User provided local file - copy it + if copy_logo(Path(logo_file), logo_path): + result["files_created"].append(str(logo_path)) + else: + result["warnings"].append(f"Failed to copy logo from: {logo_file}") + elif auto_logo: + # Search for potential sources but DO NOT auto-download + # Just inform the user about what was found + print(f" Searching for potential logo sources for '{display_name}'...") + potential_sources = search_potential_logo_sources(provider, display_name) + if potential_sources: + result["logo_suggestions"] = potential_sources + else: + result["warnings"].append( + f"Could not find logo for '{display_name}'. " + f"Please add manually to: {logo_path}" + ) + + # Generate parameter class stub if requested + if add_param_class: + result["param_class_stub"] = PARAMETER_CLASS_TEMPLATE.format( + param_class=param_class, + provider=provider.lower(), + ) + + return result + + +def main(): + parser = argparse.ArgumentParser( + description="Initialize a new LLM adapter for unstract/sdk1" + ) + parser.add_argument( + "--provider", + required=True, + help="Provider identifier (lowercase, e.g., 'newprovider')" + ) + parser.add_argument( + "--name", + required=True, + help="Display name for the provider (e.g., 'New Provider')" + ) + parser.add_argument( + "--description", + required=True, + help="Description of the adapter" + ) + parser.add_argument( + "--add-param-class", + action="store_true", + help="Generate a parameter class stub for base1.py" + ) + parser.add_argument( + "--use-param-class", + help="Use an existing parameter class (e.g., 'OpenAILLMParameters')" + ) + parser.add_argument( + "--logo-url", + help="URL to download the provider logo from" + ) + parser.add_argument( + "--logo-file", + help="Path to a local logo file to copy" + ) + parser.add_argument( + "--auto-logo", + action="store_true", + help="Automatically search for and download provider logo" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be created without actually creating files" + ) + + args = parser.parse_args() + + icon_name = to_icon_name(args.name) + + print(f"Creating LLM adapter for: {args.name}") + print(f"Provider: {args.provider}") + print(f"SDK1 Path: {SDK1_ADAPTERS}") + print() + + if args.dry_run: + print("[DRY RUN] Would create:") + print(f" - {SDK1_ADAPTERS}/llm1/{args.provider.lower()}.py") + print(f" - {SDK1_ADAPTERS}/llm1/static/{args.provider.lower()}.json") + if args.logo_url or args.logo_file or args.auto_logo: + print(f" - {ICONS_DIR}/{icon_name}.png (if logo found)") + if args.add_param_class: + print(f" - Parameter class stub for base1.py") + return 0 + + result = create_llm_adapter( + provider=args.provider, + display_name=args.name, + description=args.description, + add_param_class=args.add_param_class, + use_existing_param_class=args.use_param_class, + logo_url=args.logo_url, + logo_file=args.logo_file, + auto_logo=args.auto_logo, + ) + + if result["errors"]: + print("Errors:") + for error in result["errors"]: + print(f" - {error}") + return 1 + + if result["warnings"]: + print("Warnings:") + for warning in result["warnings"]: + print(f" - {warning}") + + print("Files created:") + for file in result["files_created"]: + print(f" - {file}") + + # Show logo suggestions if any were found + if result.get("logo_suggestions"): + print("\nPotential logo sources found (please verify before using):") + for i, suggestion in enumerate(result["logo_suggestions"], 1): + print(f" {i}. {suggestion['source']}") + print(f" URL: {suggestion['url']}") + print(f"\nTo use a logo, re-run with: --logo-url ") + print(f"Logo will be saved to: {ICONS_DIR}/{icon_name}.png") + + if result["param_class_stub"]: + print("\nParameter class stub (add to base1.py):") + print("-" * 60) + print(result["param_class_stub"]) + print("-" * 60) + + print("\nNext steps:") + print("1. Customize the JSON schema in static/{provider}.json") + print("2. If needed, add parameter class to base1.py") + print("3. Update the adapter to use the correct parameter class") + if not any("png" in f for f in result["files_created"]) and not result.get("logo_suggestions"): + print(f"4. Add logo manually to: {ICONS_DIR}/{icon_name}.png") + print("5. Test with: from unstract.sdk1.adapters.adapterkit import Adapterkit") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/skills/unstract-adapter-extension/scripts/manage_models.py b/.claude/skills/unstract-adapter-extension/scripts/manage_models.py new file mode 100755 index 0000000000..b8163489be --- /dev/null +++ b/.claude/skills/unstract-adapter-extension/scripts/manage_models.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""Manage models in existing adapter JSON schemas. + +Usage: + # Add models to dropdown enum + python manage_models.py --adapter llm --provider openai --action add-enum --models "gpt-4-turbo,gpt-4o" + + # Remove models from dropdown enum + python manage_models.py --adapter llm --provider openai --action remove-enum --models "gpt-3.5-turbo" + + # Set default model + python manage_models.py --adapter llm --provider openai --action set-default --models "gpt-4o" + + # Update model description + python manage_models.py --adapter llm --provider openai --action update-description \ + --description "Available models: gpt-4o, gpt-4-turbo, gpt-3.5-turbo" + + # List current models + python manage_models.py --adapter llm --provider openai --action list +""" + +import argparse +import json +import sys +from pathlib import Path + +# Resolve paths +SCRIPT_DIR = Path(__file__).parent +SKILL_DIR = SCRIPT_DIR.parent +REPO_ROOT = SKILL_DIR.parent.parent.parent +SDK1_ADAPTERS = REPO_ROOT / "unstract" / "sdk1" / "src" / "unstract" / "sdk1" / "adapters" + + +def get_schema_path(adapter_type: str, provider: str) -> Path: + """Get the JSON schema path for an adapter.""" + adapter_dir = "llm1" if adapter_type == "llm" else "embedding1" + return SDK1_ADAPTERS / adapter_dir / "static" / f"{provider.lower()}.json" + + +def load_schema(schema_path: Path) -> dict: + """Load and parse a JSON schema file.""" + with open(schema_path) as f: + return json.load(f) + + +def save_schema(schema_path: Path, schema: dict) -> None: + """Save a JSON schema file with proper formatting.""" + with open(schema_path, "w") as f: + json.dump(schema, f, indent=2) + f.write("\n") + + +def list_models(schema: dict) -> dict: + """Extract model information from schema.""" + model_prop = schema.get("properties", {}).get("model", {}) + return { + "type": model_prop.get("type", "unknown"), + "default": model_prop.get("default"), + "enum": model_prop.get("enum"), + "description": model_prop.get("description"), + } + + +def add_enum_models(schema: dict, models: list[str]) -> dict: + """Add models to the enum list (creates enum if doesn't exist).""" + if "properties" not in schema: + schema["properties"] = {} + if "model" not in schema["properties"]: + schema["properties"]["model"] = {"type": "string", "title": "Model"} + + model_prop = schema["properties"]["model"] + + # Get existing enum or create new one + existing_enum = model_prop.get("enum", []) + if not isinstance(existing_enum, list): + existing_enum = [] + + # Add new models (avoiding duplicates) + for model in models: + if model not in existing_enum: + existing_enum.append(model) + + model_prop["enum"] = existing_enum + + # Set default if not set + if "default" not in model_prop and existing_enum: + model_prop["default"] = existing_enum[0] + + return schema + + +def remove_enum_models(schema: dict, models: list[str]) -> dict: + """Remove models from the enum list.""" + model_prop = schema.get("properties", {}).get("model", {}) + existing_enum = model_prop.get("enum", []) + + if not existing_enum: + return schema + + # Remove specified models + updated_enum = [m for m in existing_enum if m not in models] + model_prop["enum"] = updated_enum + + # Update default if it was removed + if model_prop.get("default") in models and updated_enum: + model_prop["default"] = updated_enum[0] + elif not updated_enum: + # Remove enum entirely if no models left + if "enum" in model_prop: + del model_prop["enum"] + + return schema + + +def set_default_model(schema: dict, model: str) -> dict: + """Set the default model.""" + if "properties" not in schema: + schema["properties"] = {} + if "model" not in schema["properties"]: + schema["properties"]["model"] = {"type": "string", "title": "Model"} + + schema["properties"]["model"]["default"] = model + return schema + + +def update_description(schema: dict, description: str) -> dict: + """Update the model field description.""" + if "properties" not in schema: + schema["properties"] = {} + if "model" not in schema["properties"]: + schema["properties"]["model"] = {"type": "string", "title": "Model"} + + schema["properties"]["model"]["description"] = description + return schema + + +def convert_to_enum(schema: dict) -> dict: + """Convert free-text model field to enum dropdown.""" + model_prop = schema.get("properties", {}).get("model", {}) + + if "enum" in model_prop: + print("Model field already has enum defined") + return schema + + # Get current default or prompt for models + current_default = model_prop.get("default", "") + + print(f"Current default: {current_default}") + print("To convert to enum, use --action add-enum with --models") + + return schema + + +def convert_to_freetext(schema: dict) -> dict: + """Convert enum dropdown to free-text model field.""" + model_prop = schema.get("properties", {}).get("model", {}) + + if "enum" in model_prop: + # Preserve default if it exists + default = model_prop.get("default", model_prop["enum"][0] if model_prop["enum"] else "") + del model_prop["enum"] + model_prop["default"] = default + + return schema + + +def main(): + parser = argparse.ArgumentParser( + description="Manage models in adapter JSON schemas" + ) + parser.add_argument( + "--adapter", + required=True, + choices=["llm", "embedding"], + help="Adapter type (llm or embedding)" + ) + parser.add_argument( + "--provider", + required=True, + help="Provider name (e.g., 'openai', 'anthropic')" + ) + parser.add_argument( + "--action", + required=True, + choices=["list", "add-enum", "remove-enum", "set-default", "update-description", "to-enum", "to-freetext"], + help="Action to perform" + ) + parser.add_argument( + "--models", + help="Comma-separated list of models (for add/remove/set-default)" + ) + parser.add_argument( + "--description", + help="New description for model field" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show changes without applying them" + ) + + args = parser.parse_args() + + # Get schema path + schema_path = get_schema_path(args.adapter, args.provider) + + if not schema_path.exists(): + print(f"Error: Schema file not found: {schema_path}") + return 1 + + # Load schema + schema = load_schema(schema_path) + original_schema = json.dumps(schema, indent=2) + + # Perform action + if args.action == "list": + info = list_models(schema) + print(f"Model configuration for {args.provider} {args.adapter}:") + print(f" Type: {info['type']}") + print(f" Default: {info['default']}") + if info['enum']: + print(f" Enum values: {', '.join(info['enum'])}") + else: + print(" Enum: (free text)") + if info['description']: + print(f" Description: {info['description']}") + return 0 + + elif args.action == "add-enum": + if not args.models: + print("Error: --models required for add-enum action") + return 1 + models = [m.strip() for m in args.models.split(",")] + schema = add_enum_models(schema, models) + print(f"Added models: {', '.join(models)}") + + elif args.action == "remove-enum": + if not args.models: + print("Error: --models required for remove-enum action") + return 1 + models = [m.strip() for m in args.models.split(",")] + schema = remove_enum_models(schema, models) + print(f"Removed models: {', '.join(models)}") + + elif args.action == "set-default": + if not args.models: + print("Error: --models required for set-default action (single model)") + return 1 + model = args.models.split(",")[0].strip() + schema = set_default_model(schema, model) + print(f"Set default model: {model}") + + elif args.action == "update-description": + if not args.description: + print("Error: --description required for update-description action") + return 1 + schema = update_description(schema, args.description) + print(f"Updated description") + + elif args.action == "to-enum": + schema = convert_to_enum(schema) + + elif args.action == "to-freetext": + schema = convert_to_freetext(schema) + print("Converted to free-text field") + + # Show diff if changes were made + new_schema = json.dumps(schema, indent=2) + if original_schema != new_schema: + if args.dry_run: + print("\n[DRY RUN] Would update schema:") + print("-" * 40) + print(new_schema) + print("-" * 40) + else: + save_schema(schema_path, schema) + print(f"\nUpdated: {schema_path}") + else: + print("\nNo changes made") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/frontend/public/icons/adapter-icons/AzureAIFoundry.png b/frontend/public/icons/adapter-icons/AzureAIFoundry.png new file mode 100644 index 0000000000000000000000000000000000000000..8dd3d11e2de50f3e31cef86e8c23e8110df21083 GIT binary patch literal 31417 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 AxBvhE literal 0 HcmV?d00001 diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py index db112df69c..28eaff08cd 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py +++ b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py @@ -441,9 +441,11 @@ def validate_model(adapter_metadata: dict[str, "Any"]) -> str: class AWSBedrockLLMParameters(BaseChatCompletionParameters): """See https://docs.litellm.ai/docs/providers/bedrock.""" - aws_access_key_id: str | None - aws_secret_access_key: str | None - aws_region_name: str | None + aws_access_key_id: str | None = None + aws_secret_access_key: str | None = None + aws_region_name: str | None = None + aws_profile_name: str | None = None # For AWS SSO authentication + model_id: str | None = None # For Application Inference Profile (cost tracking) max_retries: int | None = None @staticmethod @@ -601,12 +603,51 @@ class MistralLLMParameters(BaseChatCompletionParameters): """See https://docs.litellm.ai/docs/providers/mistral.""" api_key: str + reasoning_effort: str | None = None # For Magistral models: low, medium, high @staticmethod def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: adapter_metadata["model"] = MistralLLMParameters.validate_model(adapter_metadata) - return MistralLLMParameters(**adapter_metadata).model_dump() + # Handle Mistral reasoning configuration (for Magistral models) + enable_reasoning = adapter_metadata.get("enable_reasoning", False) + + # If enable_reasoning is not explicitly provided but reasoning_effort is present, + # assume reasoning was enabled in a previous validation + has_reasoning_effort = ( + "reasoning_effort" in adapter_metadata + and adapter_metadata.get("reasoning_effort") is not None + ) + if not enable_reasoning and has_reasoning_effort: + enable_reasoning = True + + # Create a copy to avoid mutating the original metadata + result_metadata = adapter_metadata.copy() + + if enable_reasoning: + reasoning_effort = adapter_metadata.get("reasoning_effort", "medium") + result_metadata["reasoning_effort"] = reasoning_effort + + # Create validation metadata excluding control fields + exclude_fields = {"enable_reasoning"} + if not enable_reasoning: + exclude_fields.add("reasoning_effort") + + validation_metadata = { + k: v for k, v in result_metadata.items() if k not in exclude_fields + } + + validated = MistralLLMParameters(**validation_metadata).model_dump() + + # Clean up result based on reasoning state + if not enable_reasoning and "reasoning_effort" in validated: + validated.pop("reasoning_effort") + elif enable_reasoning: + validated["reasoning_effort"] = result_metadata.get( + "reasoning_effort", "medium" + ) + + return validated @staticmethod def validate_model(adapter_metadata: dict[str, "Any"]) -> str: @@ -622,13 +663,20 @@ class OllamaLLMParameters(BaseChatCompletionParameters): """See https://docs.litellm.ai/docs/providers/ollama.""" api_base: str + json_mode: bool | None = False # Enable JSON mode for structured output @staticmethod def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: adapter_metadata["model"] = OllamaLLMParameters.validate_model(adapter_metadata) adapter_metadata["api_base"] = adapter_metadata.get("base_url", "") - return OllamaLLMParameters(**adapter_metadata).model_dump() + # Handle JSON mode - convert to response_format + result_metadata = adapter_metadata.copy() + json_mode = result_metadata.pop("json_mode", False) + if json_mode: + result_metadata["response_format"] = {"type": "json_object"} + + return OllamaLLMParameters(**result_metadata).model_dump() @staticmethod def validate_model(adapter_metadata: dict[str, "Any"]) -> str: @@ -640,6 +688,33 @@ def validate_model(adapter_metadata: dict[str, "Any"]) -> str: return f"ollama_chat/{model}" +class AzureAIFoundryLLMParameters(BaseChatCompletionParameters): + """Azure AI Foundry LLM parameters. + + See https://docs.litellm.ai/docs/providers/azure_ai + """ + + api_key: str + api_base: str + + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + adapter_metadata["model"] = AzureAIFoundryLLMParameters.validate_model( + adapter_metadata + ) + + return AzureAIFoundryLLMParameters(**adapter_metadata).model_dump() + + @staticmethod + def validate_model(adapter_metadata: dict[str, "Any"]) -> str: + model = adapter_metadata.get("model", "") + # Only add azure_ai/ prefix if the model doesn't already have it + if model.startswith("azure_ai/"): + return model + else: + return f"azure_ai/{model}" + + # Embedding Parameter Classes @@ -649,6 +724,7 @@ class OpenAIEmbeddingParameters(BaseEmbeddingParameters): api_key: str api_base: str | None = None embed_batch_size: int | None = 10 + dimensions: int | None = None # For text-embedding-3-* models @staticmethod def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: @@ -672,6 +748,7 @@ class AzureOpenAIEmbeddingParameters(BaseEmbeddingParameters): api_version: str | None embed_batch_size: int | None = 5 num_retries: int | None = 3 + dimensions: int | None = None # For text-embedding-3-* models @staticmethod def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/azure.json b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/azure.json index 45e5c4d06c..7363a41aab 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/azure.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/azure.json @@ -20,7 +20,7 @@ "type": "string", "title": "Model", "default": "", - "description": "Provide the name of the model you defined in Azure console. Example text-embedding-ada-002" + "description": "Provide the name of the model you defined in Azure console. Example: text-embedding-3-small, text-embedding-ada-002" }, "deployment_name": { "type": "string", @@ -47,6 +47,13 @@ "format": "uri", "description": "Provide the Azure endpoint. Example: https://.openai.azure.com/" }, + "dimensions": { + "type": "number", + "minimum": 1, + "multipleOf": 1, + "title": "Dimensions", + "description": "Output embedding dimensions. Only supported by text-embedding-3-* models. Leave empty for default dimensions." + }, "embed_batch_size": { "type": "number", "minimum": 0, diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/openai.json b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/openai.json index 0227e05adc..9be724e41f 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/openai.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/openai.json @@ -15,8 +15,8 @@ "model": { "type": "string", "title": "Model", - "default": "text-embedding-ada-002", - "description": "Provide the name of the model." + "default": "text-embedding-3-small", + "description": "Provide the name of the model. Recommended: text-embedding-3-small, text-embedding-3-large" }, "api_key": { "type": "string", @@ -30,6 +30,13 @@ "format": "uri", "default": "https://api.openai.com/v1/" }, + "dimensions": { + "type": "number", + "minimum": 1, + "multipleOf": 1, + "title": "Dimensions", + "description": "Output embedding dimensions. Only supported by text-embedding-3-* models. Leave empty for default dimensions (1536 for small, 3072 for large)." + }, "embed_batch_size": { "type": "number", "minimum": 0, diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/azure_ai_foundry.py b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/azure_ai_foundry.py new file mode 100644 index 0000000000..2a37a5e007 --- /dev/null +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/azure_ai_foundry.py @@ -0,0 +1,40 @@ +from typing import Any + +from unstract.sdk1.adapters.base1 import BaseAdapter, AzureAIFoundryLLMParameters +from unstract.sdk1.adapters.enums import AdapterTypes + + +class AzureAIFoundryLLMAdapter(AzureAIFoundryLLMParameters, BaseAdapter): + @staticmethod + def get_id() -> str: + return "azure_ai_foundry|1ee34560-ea2b-47ac-bfce-ecc4aa5a48cb" + + @staticmethod + def get_metadata() -> dict[str, Any]: + return { + "name": "Azure AI Foundry", + "version": "1.0.0", + "adapter": AzureAIFoundryLLMAdapter, + "description": "Azure AI Foundry LLM adapter", + "is_active": True, + } + + @staticmethod + def get_name() -> str: + return "Azure AI Foundry" + + @staticmethod + def get_description() -> str: + return "Azure AI Foundry LLM adapter" + + @staticmethod + def get_provider() -> str: + return "azure_ai_foundry" + + @staticmethod + def get_icon() -> str: + return "/icons/adapter-icons/AzureAIFoundry.png" + + @staticmethod + def get_adapter_type() -> AdapterTypes: + return AdapterTypes.LLM diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/azure_ai_foundry.json b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/azure_ai_foundry.json new file mode 100644 index 0000000000..54eabf8adf --- /dev/null +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/azure_ai_foundry.json @@ -0,0 +1,59 @@ +{ + "title": "Azure AI Foundry LLM", + "type": "object", + "required": [ + "adapter_name", + "api_key", + "api_base" + ], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name", + "default": "", + "description": "Provide a unique name for this adapter instance. Example: azure-ai-foundry-1" + }, + "api_key": { + "type": "string", + "title": "API Key", + "format": "password", + "description": "Your Azure AI Foundry API key from the Azure portal." + }, + "api_base": { + "type": "string", + "title": "Endpoint URL", + "format": "uri", + "default": "", + "description": "Azure AI Foundry endpoint URL. Example: https://..inference.ai.azure.com/ or https://-serverless..inference.ai.azure.com/" + }, + "model": { + "type": "string", + "title": "Model", + "default": "", + "description": "The model name deployed in Azure AI Foundry. Examples: command-r-plus, mistral-large-latest, gpt-4o" + }, + "max_tokens": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Maximum Output Tokens", + "description": "Maximum number of output tokens to limit LLM replies. Leave empty to use model default." + }, + "max_retries": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Max Retries", + "default": 5, + "description": "Maximum number of retries to attempt when a request fails." + }, + "timeout": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Timeout", + "default": 900, + "description": "Request timeout in seconds." + } + } +} diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json index ec36f285ae..9adb8b8ceb 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json @@ -2,9 +2,7 @@ "title": "Bedrock LLM", "type": "object", "required": [ - "aws_secret_access_key", "region_name", - "aws_access_key_id", "model", "adapter_name" ], @@ -24,20 +22,30 @@ "aws_access_key_id": { "type": "string", "title": "AWS Access Key ID", - "description": "Provide your AWS Access Key ID", + "description": "Provide your AWS Access Key ID. Leave empty if using AWS Profile or IAM role.", "format": "password" }, "aws_secret_access_key": { "type": "string", "title": "AWS Secret Access Key", - "description": "Provide your AWS Secret Access Key", + "description": "Provide your AWS Secret Access Key. Leave empty if using AWS Profile or IAM role.", "format": "password" }, "region_name": { "type": "string", - "title": "AWS Region name", + "title": "AWS Region Name", "description": "Provide the AWS Region name where the service is running. Eg. us-east-1" }, + "aws_profile_name": { + "type": "string", + "title": "AWS Profile Name", + "description": "AWS SSO profile name for authentication. Use this instead of access keys when using AWS SSO. Example: dev-profile" + }, + "model_id": { + "type": "string", + "title": "Application Inference Profile ARN", + "description": "Optional ARN for Application Inference Profile for cost tracking. Example: arn:aws:bedrock:us-east-1:000000000000:application-inference-profile/xxxx" + }, "max_tokens": { "type": "number", "minimum": 0, diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/mistral.json b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/mistral.json index 73739b48d8..78c0def9b3 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/mistral.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/mistral.json @@ -16,8 +16,8 @@ "model": { "type": "string", "title": "Model", - "default": "mistral-medium", - "description": "Provide the model name to be used. Example: mistral-tiny, mistral-small, mistral-medium" + "default": "mistral-large-latest", + "description": "Provide the model name. Examples: mistral-large-latest, mistral-small-latest, magistral-medium-2506, magistral-small-2506" }, "api_key": { "type": "string", @@ -48,6 +48,53 @@ "title": "Timeout", "default": 900, "description": "Timeout in seconds" + }, + "enable_reasoning": { + "type": "boolean", + "title": "Enable Reasoning", + "default": false, + "description": "Enable reasoning capabilities for Magistral models (magistral-medium-2506, magistral-small-2506). Provides step-by-step reasoning for complex tasks." + } + }, + "allOf": [ + { + "if": { + "properties": { + "enable_reasoning": { + "const": true + } + } + }, + "then": { + "properties": { + "reasoning_effort": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "default": "medium", + "title": "Reasoning Effort", + "description": "Controls the depth of reasoning. Higher values provide more thorough analysis but may increase latency and cost." + } + }, + "required": [ + "reasoning_effort" + ] + } + }, + { + "if": { + "properties": { + "enable_reasoning": { + "const": false + } + } + }, + "then": { + "properties": {} + } } - } + ] } diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json index cefbed091f..3800814c77 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json @@ -17,7 +17,7 @@ "type": "string", "title": "Model", "default": "", - "description": "Provide the model name to be used. Example:llama2, llama3, mistral" + "description": "Provide the model name to be used. Example: llama3.1, llama3, mistral, deepseek-r1" }, "base_url": { "type": "string", @@ -25,12 +25,27 @@ "default": "", "description": "Provide the base URL where Ollama server is running. Example: http://docker.host.internal:11434 or http://localhost:11434" }, + "max_tokens": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Maximum Output Tokens", + "description": "Maximum number of output tokens to generate. Maps to Ollama's num_predict parameter." + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2, + "title": "Temperature", + "default": 0.7, + "description": "Controls randomness. Lower values make output more focused and deterministic (0-2)." + }, "context_window": { "type": "number", "minimum": 0, "multipleOf": 1, - "title": "Context window", - "default":3900, + "title": "Context Window", + "default": 3900, "description": "The maximum number of context tokens for the model." }, "request_timeout": { @@ -40,6 +55,12 @@ "title": "Request Timeout", "default": 900, "description": "Request timeout in seconds" + }, + "json_mode": { + "type": "boolean", + "title": "JSON Mode", + "default": false, + "description": "Enable JSON mode to constrain output to valid JSON. Useful for structured data extraction." } } } From fb2b591f7dd7f5631a75aed5ddd6b17b3c8b1c5e Mon Sep 17 00:00:00 2001 From: Hari John Kuriakose Date: Fri, 19 Dec 2025 21:28:58 +0530 Subject: [PATCH 2/5] * feat: add unstract adapter extension skill for claude * feat: add azure ai foundry adapter * feat: update parameters in various adapters From 182464bc7e2a8099f865aca0a1d3794c2cdcbe94 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 05:41:58 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../scripts/check_adapter_updates.py | 224 +++++++++++++----- .../scripts/init_embedding_adapter.py | 186 +++++++++------ .../scripts/init_llm_adapter.py | 183 ++++++++------ .../scripts/manage_models.py | 44 ++-- .../sdk1/adapters/llm1/azure_ai_foundry.py | 2 +- 5 files changed, 398 insertions(+), 241 deletions(-) diff --git a/.claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py b/.claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py index f09fb20cbb..ec50ed7f76 100644 --- a/.claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py +++ b/.claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py @@ -27,116 +27,215 @@ "llm": { "openai": { "known_params": [ - "api_key", "api_base", "api_version", "model", "max_tokens", - "max_retries", "timeout", "temperature", "top_p", "n", - "enable_reasoning", "reasoning_effort", "seed", "response_format", - "tools", "tool_choice", "parallel_tool_calls", "logprobs" + "api_key", + "api_base", + "api_version", + "model", + "max_tokens", + "max_retries", + "timeout", + "temperature", + "top_p", + "n", + "enable_reasoning", + "reasoning_effort", + "seed", + "response_format", + "tools", + "tool_choice", + "parallel_tool_calls", + "logprobs", ], "reasoning_models": ["o1-mini", "o1-preview", "o3-mini", "o3", "o4-mini"], "latest_models": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-5"], - "docs_url": "https://docs.litellm.ai/docs/providers/openai" + "docs_url": "https://docs.litellm.ai/docs/providers/openai", }, "anthropic": { "known_params": [ - "api_key", "model", "max_tokens", "max_retries", "timeout", - "temperature", "enable_thinking", "budget_tokens", "thinking" + "api_key", + "model", + "max_tokens", + "max_retries", + "timeout", + "temperature", + "enable_thinking", + "budget_tokens", + "thinking", ], "thinking_models": ["claude-3-7-sonnet", "claude-sonnet-4", "claude-opus-4"], "latest_models": ["claude-sonnet-4-5-20250929", "claude-opus-4-1-20250805"], - "docs_url": "https://docs.litellm.ai/docs/providers/anthropic" + "docs_url": "https://docs.litellm.ai/docs/providers/anthropic", }, "azure": { "known_params": [ - "api_key", "api_base", "api_version", "deployment_name", "azure_endpoint", - "model", "max_tokens", "max_retries", "timeout", "temperature", - "enable_reasoning", "reasoning_effort" + "api_key", + "api_base", + "api_version", + "deployment_name", + "azure_endpoint", + "model", + "max_tokens", + "max_retries", + "timeout", + "temperature", + "enable_reasoning", + "reasoning_effort", ], "reasoning_models": ["o1-mini", "o1-preview"], - "docs_url": "https://docs.litellm.ai/docs/providers/azure" + "docs_url": "https://docs.litellm.ai/docs/providers/azure", }, "bedrock": { "known_params": [ - "aws_access_key_id", "aws_secret_access_key", "region_name", - "aws_region_name", "aws_profile_name", "model_id", "model", - "max_tokens", "max_retries", "timeout", "temperature", - "enable_thinking", "budget_tokens", "thinking", "top_k" + "aws_access_key_id", + "aws_secret_access_key", + "region_name", + "aws_region_name", + "aws_profile_name", + "model_id", + "model", + "max_tokens", + "max_retries", + "timeout", + "temperature", + "enable_thinking", + "budget_tokens", + "thinking", + "top_k", ], "thinking_models": ["anthropic.claude-3-7-sonnet"], - "docs_url": "https://docs.litellm.ai/docs/providers/bedrock" + "docs_url": "https://docs.litellm.ai/docs/providers/bedrock", }, "vertex_ai": { "known_params": [ - "json_credentials", "vertex_credentials", "project", "vertex_project", - "model", "max_tokens", "max_retries", "timeout", "temperature", - "safety_settings", "enable_thinking", "budget_tokens", "thinking", - "reasoning_effort", "tools", "googleSearch" + "json_credentials", + "vertex_credentials", + "project", + "vertex_project", + "model", + "max_tokens", + "max_retries", + "timeout", + "temperature", + "safety_settings", + "enable_thinking", + "budget_tokens", + "thinking", + "reasoning_effort", + "tools", + "googleSearch", ], "thinking_models": ["gemini-2.5-flash-preview", "gemini-2.5-pro"], - "docs_url": "https://docs.litellm.ai/docs/providers/vertex" + "docs_url": "https://docs.litellm.ai/docs/providers/vertex", }, "mistral": { "known_params": [ - "api_key", "model", "max_tokens", "max_retries", "timeout", - "temperature", "enable_reasoning", "reasoning_effort", "tools" + "api_key", + "model", + "max_tokens", + "max_retries", + "timeout", + "temperature", + "enable_reasoning", + "reasoning_effort", + "tools", ], "reasoning_models": ["magistral-medium-2506", "magistral-small-2506"], "latest_models": ["mistral-large-latest", "mistral-small-latest"], - "docs_url": "https://docs.litellm.ai/docs/providers/mistral" + "docs_url": "https://docs.litellm.ai/docs/providers/mistral", }, "ollama": { "known_params": [ - "base_url", "api_base", "model", "max_tokens", "temperature", - "context_window", "request_timeout", "json_mode", "response_format", - "tools" + "base_url", + "api_base", + "model", + "max_tokens", + "temperature", + "context_window", + "request_timeout", + "json_mode", + "response_format", + "tools", ], - "docs_url": "https://docs.litellm.ai/docs/providers/ollama" + "docs_url": "https://docs.litellm.ai/docs/providers/ollama", }, "anyscale": { "known_params": [ - "api_key", "api_base", "model", "max_tokens", "max_retries", - "timeout", "temperature", "additional_kwargs" + "api_key", + "api_base", + "model", + "max_tokens", + "max_retries", + "timeout", + "temperature", + "additional_kwargs", ], - "docs_url": "https://docs.litellm.ai/docs/providers/anyscale" - } + "docs_url": "https://docs.litellm.ai/docs/providers/anyscale", + }, }, "embedding": { "openai": { "known_params": [ - "api_key", "api_base", "model", "embed_batch_size", "timeout", - "dimensions" + "api_key", + "api_base", + "model", + "embed_batch_size", + "timeout", + "dimensions", ], "latest_models": ["text-embedding-3-small", "text-embedding-3-large"], - "docs_url": "https://docs.litellm.ai/docs/embedding/supported_embedding" + "docs_url": "https://docs.litellm.ai/docs/embedding/supported_embedding", }, "azure": { "known_params": [ - "api_key", "api_base", "api_version", "deployment_name", - "azure_endpoint", "model", "embed_batch_size", "timeout", - "dimensions" + "api_key", + "api_base", + "api_version", + "deployment_name", + "azure_endpoint", + "model", + "embed_batch_size", + "timeout", + "dimensions", ], - "docs_url": "https://docs.litellm.ai/docs/providers/azure" + "docs_url": "https://docs.litellm.ai/docs/providers/azure", }, "bedrock": { "known_params": [ - "aws_access_key_id", "aws_secret_access_key", "region_name", - "aws_region_name", "model", "max_retries", "timeout" + "aws_access_key_id", + "aws_secret_access_key", + "region_name", + "aws_region_name", + "model", + "max_retries", + "timeout", ], - "docs_url": "https://docs.litellm.ai/docs/providers/bedrock_embedding" + "docs_url": "https://docs.litellm.ai/docs/providers/bedrock_embedding", }, "vertexai": { "known_params": [ - "json_credentials", "vertex_credentials", "project", "vertex_project", - "model", "embed_batch_size", "embed_mode", "dimensions", "input_type" + "json_credentials", + "vertex_credentials", + "project", + "vertex_project", + "model", + "embed_batch_size", + "embed_mode", + "dimensions", + "input_type", ], - "docs_url": "https://docs.litellm.ai/docs/providers/vertex" + "docs_url": "https://docs.litellm.ai/docs/providers/vertex", }, "ollama": { "known_params": [ - "base_url", "api_base", "model_name", "model", "embed_batch_size" + "base_url", + "api_base", + "model_name", + "model", + "embed_batch_size", ], - "docs_url": "https://docs.litellm.ai/docs/providers/ollama" - } - } + "docs_url": "https://docs.litellm.ai/docs/providers/ollama", + }, + }, } @@ -179,7 +278,7 @@ def analyze_adapter(adapter_type: str, provider: str) -> dict: "current_properties": [], "missing_properties": [], "suggestions": [], - "docs_url": None + "docs_url": None, } # Load schema @@ -203,7 +302,9 @@ def analyze_adapter(adapter_type: str, provider: str) -> dict: # Find missing parameters known_params = set(features.get("known_params", [])) - missing = known_params - current_props - {"adapter_name"} # adapter_name is always present + missing = ( + known_params - current_props - {"adapter_name"} + ) # adapter_name is always present # Filter out params that might be named differently common_aliases = { @@ -211,7 +312,7 @@ def analyze_adapter(adapter_type: str, provider: str) -> dict: "base_url": "api_base", "vertex_credentials": "json_credentials", "vertex_project": "project", - "aws_region_name": "region_name" + "aws_region_name": "region_name", } filtered_missing = set() @@ -235,7 +336,11 @@ def analyze_adapter(adapter_type: str, provider: str) -> dict: reasoning_models = features.get("reasoning_models", []) thinking_models = features.get("thinking_models", []) - if reasoning_models and "enable_reasoning" not in current_props and "reasoning_effort" not in current_props: + if ( + reasoning_models + and "enable_reasoning" not in current_props + and "reasoning_effort" not in current_props + ): result["suggestions"].append( f"Consider adding reasoning support for models: {', '.join(reasoning_models)}" ) @@ -326,17 +431,12 @@ def main(): "--adapter", choices=["llm", "embedding", "all"], default="all", - help="Type of adapter to check" - ) - parser.add_argument( - "--provider", - help="Specific provider to check (e.g., openai, anthropic)" + help="Type of adapter to check", ) parser.add_argument( - "--json", - action="store_true", - help="Output results as JSON" + "--provider", help="Specific provider to check (e.g., openai, anthropic)" ) + parser.add_argument("--json", action="store_true", help="Output results as JSON") args = parser.parse_args() diff --git a/.claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py b/.claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py index c37344f7de..0122c39ed7 100755 --- a/.claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py +++ b/.claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py @@ -18,18 +18,20 @@ import sys import uuid from pathlib import Path -from urllib.request import urlopen, Request -from urllib.error import URLError, HTTPError +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen # Resolve paths SCRIPT_DIR = Path(__file__).parent SKILL_DIR = SCRIPT_DIR.parent # Find the sdk1 adapters directory relative to the repo root -REPO_ROOT = SKILL_DIR.parent.parent.parent # .claude/skills/unstract-adapter-extension -> repo root +REPO_ROOT = ( + SKILL_DIR.parent.parent.parent +) # .claude/skills/unstract-adapter-extension -> repo root SDK1_ADAPTERS = REPO_ROOT / "unstract" / "sdk1" / "src" / "unstract" / "sdk1" / "adapters" ICONS_DIR = REPO_ROOT / "frontend" / "public" / "icons" / "adapter-icons" -EMBEDDING_ADAPTER_TEMPLATE = '''from typing import Any +EMBEDDING_ADAPTER_TEMPLATE = """from typing import Any from unstract.sdk1.adapters.base1 import BaseAdapter, {param_class} from unstract.sdk1.adapters.enums import AdapterTypes @@ -69,7 +71,7 @@ def get_icon() -> str: @staticmethod def get_adapter_type() -> AdapterTypes: return AdapterTypes.EMBEDDING -''' +""" EMBEDDING_SCHEMA_TEMPLATE = { "title": "{display_name} Embedding", @@ -80,27 +82,27 @@ def get_adapter_type() -> AdapterTypes: "type": "string", "title": "Name", "default": "", - "description": "Provide a unique name for this adapter instance. Example: {provider}-emb-1" + "description": "Provide a unique name for this adapter instance. Example: {provider}-emb-1", }, "model": { "type": "string", "title": "Model", "default": "", - "description": "Provide the name of the embedding model." + "description": "Provide the name of the embedding model.", }, "api_key": { "type": "string", "title": "API Key", "default": "", "format": "password", - "description": "Your {display_name} API key." + "description": "Your {display_name} API key.", }, "api_base": { "type": "string", "title": "API Base", "format": "uri", "default": "", - "description": "API endpoint URL (if different from default)." + "description": "API endpoint URL (if different from default).", }, "embed_batch_size": { "type": "number", @@ -108,7 +110,7 @@ def get_adapter_type() -> AdapterTypes: "multipleOf": 1, "title": "Embed Batch Size", "default": 10, - "description": "Number of texts to embed in each batch." + "description": "Number of texts to embed in each batch.", }, "timeout": { "type": "number", @@ -116,9 +118,9 @@ def get_adapter_type() -> AdapterTypes: "multipleOf": 1, "title": "Timeout", "default": 240, - "description": "Timeout in seconds" - } - } + "description": "Timeout in seconds", + }, + }, } PARAMETER_CLASS_TEMPLATE = ''' @@ -156,7 +158,9 @@ def to_class_name(provider: str) -> str: if provider.lower() in special_cases: return special_cases[provider.lower()] - return "".join(word.capitalize() for word in provider.replace("_", " ").replace("-", " ").split()) + return "".join( + word.capitalize() for word in provider.replace("_", " ").replace("-", " ").split() + ) def to_icon_name(display_name: str) -> str: @@ -168,7 +172,7 @@ def fetch_url(url: str, timeout: int = 10) -> bytes | None: """Fetch content from URL with error handling.""" try: headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } request = Request(url, headers=headers) with urlopen(request, timeout=timeout) as response: @@ -201,14 +205,13 @@ def search_potential_logo_sources(provider: str, display_name: str) -> list[dict for domain, source_type in domains: url = f"https://logo.clearbit.com/{domain}" try: - headers = {'User-Agent': 'Mozilla/5.0'} - request = Request(url, headers=headers, method='HEAD') + headers = {"User-Agent": "Mozilla/5.0"} + request = Request(url, headers=headers, method="HEAD") with urlopen(request, timeout=5) as response: if response.status == 200: - found_sources.append({ - 'url': url, - 'source': f"Clearbit ({source_type}: {domain})" - }) + found_sources.append( + {"url": url, "source": f"Clearbit ({source_type}: {domain})"} + ) except (URLError, HTTPError, TimeoutError): continue @@ -222,23 +225,24 @@ def search_potential_logo_sources(provider: str, display_name: str) -> list[dict for name in github_names: url = f"https://github.com/{name}.png?size=512" try: - headers = {'User-Agent': 'Mozilla/5.0'} - request = Request(url, headers=headers, method='HEAD') + headers = {"User-Agent": "Mozilla/5.0"} + request = Request(url, headers=headers, method="HEAD") with urlopen(request, timeout=5) as response: if response.status == 200: - content_type = response.headers.get('Content-Type', '') - if 'image' in content_type: - found_sources.append({ - 'url': url, - 'source': f"GitHub avatar (@{name})" - }) + content_type = response.headers.get("Content-Type", "") + if "image" in content_type: + found_sources.append( + {"url": url, "source": f"GitHub avatar (@{name})"} + ) except (URLError, HTTPError, TimeoutError): continue return found_sources -def download_and_process_logo(url: str, output_path: Path, target_size: int = 512) -> bool: +def download_and_process_logo( + url: str, output_path: Path, target_size: int = 512 +) -> bool: """Download logo from URL and process to standard format. Args: @@ -256,60 +260,78 @@ def download_and_process_logo(url: str, output_path: Path, target_size: int = 51 return False # Check if SVG (by URL extension or content) - is_svg = url.lower().endswith('.svg') or image_data[:5] == b' b return False # Check if SVG - is_svg = source_path.suffix.lower() == '.svg' + is_svg = source_path.suffix.lower() == ".svg" if is_svg: # Use ImageMagick for SVG conversion with optimal settings @@ -336,17 +358,30 @@ def copy_logo(source_path: Path, output_path: Path, target_size: int = 512) -> b try: result = subprocess.run( - ['magick', '-density', '4800', '-background', 'none', - str(source_path), '-resize', f'{target_size}x{target_size}', - '-depth', '8', str(output_path)], - capture_output=True, text=True + [ + "magick", + "-density", + "4800", + "-background", + "none", + str(source_path), + "-resize", + f"{target_size}x{target_size}", + "-depth", + "8", + str(output_path), + ], + capture_output=True, + text=True, ) if result.returncode != 0: print(f" ImageMagick error: {result.stderr}") return False return True except FileNotFoundError: - print(" Note: ImageMagick not found. Install with: sudo pacman -S imagemagick") + print( + " Note: ImageMagick not found. Install with: sudo pacman -S imagemagick" + ) return False # Handle raster images with PIL @@ -354,24 +389,25 @@ def copy_logo(source_path: Path, output_path: Path, target_size: int = 512) -> b from PIL import Image img = Image.open(source_path) - if img.mode != 'RGBA': - img = img.convert('RGBA') + if img.mode != "RGBA": + img = img.convert("RGBA") if img.width != target_size or img.height != target_size: ratio = min(target_size / img.width, target_size / img.height) new_size = (int(img.width * ratio), int(img.height * ratio)) img = img.resize(new_size, Image.Resampling.LANCZOS) - canvas = Image.new('RGBA', (target_size, target_size), (255, 255, 255, 0)) + canvas = Image.new("RGBA", (target_size, target_size), (255, 255, 255, 0)) offset = ((target_size - img.width) // 2, (target_size - img.height) // 2) - canvas.paste(img, offset, img if img.mode == 'RGBA' else None) + canvas.paste(img, offset, img if img.mode == "RGBA" else None) img = canvas - img.save(output_path, 'PNG') + img.save(output_path, "PNG") return True except ImportError: import shutil + shutil.copy2(source_path, output_path) return True except Exception: @@ -403,7 +439,9 @@ def create_embedding_adapter( static_dir = embedding_dir / "static" if not embedding_dir.exists(): - result["errors"].append(f"Embedding adapters directory not found at: {embedding_dir}") + result["errors"].append( + f"Embedding adapters directory not found at: {embedding_dir}" + ) return result static_dir.mkdir(exist_ok=True) @@ -498,44 +536,34 @@ def main(): parser.add_argument( "--provider", required=True, - help="Provider identifier (lowercase, e.g., 'newprovider')" + help="Provider identifier (lowercase, e.g., 'newprovider')", ) parser.add_argument( "--name", required=True, - help="Display name for the provider (e.g., 'New Provider')" - ) - parser.add_argument( - "--description", - required=True, - help="Description of the adapter" + help="Display name for the provider (e.g., 'New Provider')", ) + parser.add_argument("--description", required=True, help="Description of the adapter") parser.add_argument( "--add-param-class", action="store_true", - help="Generate a parameter class stub for base1.py" + help="Generate a parameter class stub for base1.py", ) parser.add_argument( "--use-param-class", - help="Use an existing parameter class (e.g., 'OpenAIEmbeddingParameters')" - ) - parser.add_argument( - "--logo-url", - help="URL to download the provider logo from" - ) - parser.add_argument( - "--logo-file", - help="Path to a local logo file to copy" + help="Use an existing parameter class (e.g., 'OpenAIEmbeddingParameters')", ) + parser.add_argument("--logo-url", help="URL to download the provider logo from") + parser.add_argument("--logo-file", help="Path to a local logo file to copy") parser.add_argument( "--auto-logo", action="store_true", - help="Automatically search for and download provider logo" + help="Automatically search for and download provider logo", ) parser.add_argument( "--dry-run", action="store_true", - help="Show what would be created without actually creating files" + help="Show what would be created without actually creating files", ) args = parser.parse_args() @@ -554,7 +582,7 @@ def main(): if args.logo_url or args.logo_file or args.auto_logo: print(f" - {ICONS_DIR}/{icon_name}.png (if logo found)") if args.add_param_class: - print(f" - Parameter class stub for base1.py") + print(" - Parameter class stub for base1.py") return 0 result = create_embedding_adapter( @@ -589,7 +617,7 @@ def main(): for i, suggestion in enumerate(result["logo_suggestions"], 1): print(f" {i}. {suggestion['source']}") print(f" URL: {suggestion['url']}") - print(f"\nTo use a logo, re-run with: --logo-url ") + print("\nTo use a logo, re-run with: --logo-url ") print(f"Logo will be saved to: {ICONS_DIR}/{icon_name}.png") if result["param_class_stub"]: @@ -602,7 +630,9 @@ def main(): print("1. Customize the JSON schema in static/{provider}.json") print("2. If needed, add parameter class to base1.py") print("3. Update the adapter to use the correct parameter class") - if not any("png" in f for f in result["files_created"]) and not result.get("logo_suggestions"): + if not any("png" in f for f in result["files_created"]) and not result.get( + "logo_suggestions" + ): print(f"4. Add logo manually to: {ICONS_DIR}/{icon_name}.png") print("5. Test with: from unstract.sdk1.adapters.adapterkit import Adapterkit") diff --git a/.claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py b/.claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py index 956adb1701..202bb9a288 100755 --- a/.claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py +++ b/.claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py @@ -15,22 +15,23 @@ import argparse import json -import re import sys import uuid from pathlib import Path -from urllib.request import urlopen, Request -from urllib.error import URLError, HTTPError +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen # Resolve paths SCRIPT_DIR = Path(__file__).parent SKILL_DIR = SCRIPT_DIR.parent # Find the sdk1 adapters directory relative to the repo root -REPO_ROOT = SKILL_DIR.parent.parent.parent # .claude/skills/unstract-adapter-extension -> repo root +REPO_ROOT = ( + SKILL_DIR.parent.parent.parent +) # .claude/skills/unstract-adapter-extension -> repo root SDK1_ADAPTERS = REPO_ROOT / "unstract" / "sdk1" / "src" / "unstract" / "sdk1" / "adapters" ICONS_DIR = REPO_ROOT / "frontend" / "public" / "icons" / "adapter-icons" -LLM_ADAPTER_TEMPLATE = '''from typing import Any +LLM_ADAPTER_TEMPLATE = """from typing import Any from unstract.sdk1.adapters.base1 import BaseAdapter, {param_class} from unstract.sdk1.adapters.enums import AdapterTypes @@ -70,7 +71,7 @@ def get_icon() -> str: @staticmethod def get_adapter_type() -> AdapterTypes: return AdapterTypes.LLM -''' +""" LLM_SCHEMA_TEMPLATE = { "title": "{display_name} LLM", @@ -81,26 +82,26 @@ def get_adapter_type() -> AdapterTypes: "type": "string", "title": "Name", "default": "", - "description": "Provide a unique name for this adapter instance. Example: {provider}-llm-1" + "description": "Provide a unique name for this adapter instance. Example: {provider}-llm-1", }, "api_key": { "type": "string", "title": "API Key", "format": "password", - "description": "Your {display_name} API key." + "description": "Your {display_name} API key.", }, "model": { "type": "string", "title": "Model", "default": "", - "description": "The model to use for the API request." + "description": "The model to use for the API request.", }, "max_tokens": { "type": "number", "minimum": 0, "multipleOf": 1, "title": "Maximum Output Tokens", - "description": "Maximum number of output tokens to limit LLM replies." + "description": "Maximum number of output tokens to limit LLM replies.", }, "max_retries": { "type": "number", @@ -108,7 +109,7 @@ def get_adapter_type() -> AdapterTypes: "multipleOf": 1, "title": "Max Retries", "default": 5, - "description": "The maximum number of times to retry a request if it fails." + "description": "The maximum number of times to retry a request if it fails.", }, "timeout": { "type": "number", @@ -116,9 +117,9 @@ def get_adapter_type() -> AdapterTypes: "multipleOf": 1, "title": "Timeout", "default": 900, - "description": "Timeout in seconds" - } - } + "description": "Timeout in seconds", + }, + }, } PARAMETER_CLASS_TEMPLATE = ''' @@ -160,7 +161,9 @@ def to_class_name(provider: str) -> str: return special_cases[provider.lower()] # Default: capitalize each word - return "".join(word.capitalize() for word in provider.replace("_", " ").replace("-", " ").split()) + return "".join( + word.capitalize() for word in provider.replace("_", " ").replace("-", " ").split() + ) def to_icon_name(display_name: str) -> str: @@ -175,7 +178,7 @@ def fetch_url(url: str, timeout: int = 10) -> bytes | None: """Fetch content from URL with error handling.""" try: headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } request = Request(url, headers=headers) with urlopen(request, timeout=timeout) as response: @@ -208,14 +211,13 @@ def search_potential_logo_sources(provider: str, display_name: str) -> list[dict for domain, source_type in domains: url = f"https://logo.clearbit.com/{domain}" try: - headers = {'User-Agent': 'Mozilla/5.0'} - request = Request(url, headers=headers, method='HEAD') + headers = {"User-Agent": "Mozilla/5.0"} + request = Request(url, headers=headers, method="HEAD") with urlopen(request, timeout=5) as response: if response.status == 200: - found_sources.append({ - 'url': url, - 'source': f"Clearbit ({source_type}: {domain})" - }) + found_sources.append( + {"url": url, "source": f"Clearbit ({source_type}: {domain})"} + ) except (URLError, HTTPError, TimeoutError): continue @@ -229,23 +231,24 @@ def search_potential_logo_sources(provider: str, display_name: str) -> list[dict for name in github_names: url = f"https://github.com/{name}.png?size=512" try: - headers = {'User-Agent': 'Mozilla/5.0'} - request = Request(url, headers=headers, method='HEAD') + headers = {"User-Agent": "Mozilla/5.0"} + request = Request(url, headers=headers, method="HEAD") with urlopen(request, timeout=5) as response: if response.status == 200: - content_type = response.headers.get('Content-Type', '') - if 'image' in content_type: - found_sources.append({ - 'url': url, - 'source': f"GitHub avatar (@{name})" - }) + content_type = response.headers.get("Content-Type", "") + if "image" in content_type: + found_sources.append( + {"url": url, "source": f"GitHub avatar (@{name})"} + ) except (URLError, HTTPError, TimeoutError): continue return found_sources -def download_and_process_logo(url: str, output_path: Path, target_size: int = 512) -> bool: +def download_and_process_logo( + url: str, output_path: Path, target_size: int = 512 +) -> bool: """Download logo from URL and process to standard format. Args: @@ -263,60 +266,78 @@ def download_and_process_logo(url: str, output_path: Path, target_size: int = 51 return False # Check if SVG (by URL extension or content) - is_svg = url.lower().endswith('.svg') or image_data[:5] == b' b return False # Check if SVG - is_svg = source_path.suffix.lower() == '.svg' + is_svg = source_path.suffix.lower() == ".svg" if is_svg: # Use ImageMagick for SVG conversion with optimal settings @@ -343,17 +364,30 @@ def copy_logo(source_path: Path, output_path: Path, target_size: int = 512) -> b try: result = subprocess.run( - ['magick', '-density', '4800', '-background', 'none', - str(source_path), '-resize', f'{target_size}x{target_size}', - '-depth', '8', str(output_path)], - capture_output=True, text=True + [ + "magick", + "-density", + "4800", + "-background", + "none", + str(source_path), + "-resize", + f"{target_size}x{target_size}", + "-depth", + "8", + str(output_path), + ], + capture_output=True, + text=True, ) if result.returncode != 0: print(f" ImageMagick error: {result.stderr}") return False return True except FileNotFoundError: - print(" Note: ImageMagick not found. Install with: sudo pacman -S imagemagick") + print( + " Note: ImageMagick not found. Install with: sudo pacman -S imagemagick" + ) return False # Handle raster images with PIL @@ -361,24 +395,25 @@ def copy_logo(source_path: Path, output_path: Path, target_size: int = 512) -> b from PIL import Image img = Image.open(source_path) - if img.mode != 'RGBA': - img = img.convert('RGBA') + if img.mode != "RGBA": + img = img.convert("RGBA") if img.width != target_size or img.height != target_size: ratio = min(target_size / img.width, target_size / img.height) new_size = (int(img.width * ratio), int(img.height * ratio)) img = img.resize(new_size, Image.Resampling.LANCZOS) - canvas = Image.new('RGBA', (target_size, target_size), (255, 255, 255, 0)) + canvas = Image.new("RGBA", (target_size, target_size), (255, 255, 255, 0)) offset = ((target_size - img.width) // 2, (target_size - img.height) // 2) - canvas.paste(img, offset, img if img.mode == 'RGBA' else None) + canvas.paste(img, offset, img if img.mode == "RGBA" else None) img = canvas - img.save(output_path, 'PNG') + img.save(output_path, "PNG") return True except ImportError: import shutil + shutil.copy2(source_path, output_path) return True except Exception: @@ -511,44 +546,34 @@ def main(): parser.add_argument( "--provider", required=True, - help="Provider identifier (lowercase, e.g., 'newprovider')" + help="Provider identifier (lowercase, e.g., 'newprovider')", ) parser.add_argument( "--name", required=True, - help="Display name for the provider (e.g., 'New Provider')" - ) - parser.add_argument( - "--description", - required=True, - help="Description of the adapter" + help="Display name for the provider (e.g., 'New Provider')", ) + parser.add_argument("--description", required=True, help="Description of the adapter") parser.add_argument( "--add-param-class", action="store_true", - help="Generate a parameter class stub for base1.py" + help="Generate a parameter class stub for base1.py", ) parser.add_argument( "--use-param-class", - help="Use an existing parameter class (e.g., 'OpenAILLMParameters')" - ) - parser.add_argument( - "--logo-url", - help="URL to download the provider logo from" - ) - parser.add_argument( - "--logo-file", - help="Path to a local logo file to copy" + help="Use an existing parameter class (e.g., 'OpenAILLMParameters')", ) + parser.add_argument("--logo-url", help="URL to download the provider logo from") + parser.add_argument("--logo-file", help="Path to a local logo file to copy") parser.add_argument( "--auto-logo", action="store_true", - help="Automatically search for and download provider logo" + help="Automatically search for and download provider logo", ) parser.add_argument( "--dry-run", action="store_true", - help="Show what would be created without actually creating files" + help="Show what would be created without actually creating files", ) args = parser.parse_args() @@ -567,7 +592,7 @@ def main(): if args.logo_url or args.logo_file or args.auto_logo: print(f" - {ICONS_DIR}/{icon_name}.png (if logo found)") if args.add_param_class: - print(f" - Parameter class stub for base1.py") + print(" - Parameter class stub for base1.py") return 0 result = create_llm_adapter( @@ -602,7 +627,7 @@ def main(): for i, suggestion in enumerate(result["logo_suggestions"], 1): print(f" {i}. {suggestion['source']}") print(f" URL: {suggestion['url']}") - print(f"\nTo use a logo, re-run with: --logo-url ") + print("\nTo use a logo, re-run with: --logo-url ") print(f"Logo will be saved to: {ICONS_DIR}/{icon_name}.png") if result["param_class_stub"]: @@ -615,7 +640,9 @@ def main(): print("1. Customize the JSON schema in static/{provider}.json") print("2. If needed, add parameter class to base1.py") print("3. Update the adapter to use the correct parameter class") - if not any("png" in f for f in result["files_created"]) and not result.get("logo_suggestions"): + if not any("png" in f for f in result["files_created"]) and not result.get( + "logo_suggestions" + ): print(f"4. Add logo manually to: {ICONS_DIR}/{icon_name}.png") print("5. Test with: from unstract.sdk1.adapters.adapterkit import Adapterkit") diff --git a/.claude/skills/unstract-adapter-extension/scripts/manage_models.py b/.claude/skills/unstract-adapter-extension/scripts/manage_models.py index b8163489be..933954b25b 100755 --- a/.claude/skills/unstract-adapter-extension/scripts/manage_models.py +++ b/.claude/skills/unstract-adapter-extension/scripts/manage_models.py @@ -157,7 +157,9 @@ def convert_to_freetext(schema: dict) -> dict: if "enum" in model_prop: # Preserve default if it exists - default = model_prop.get("default", model_prop["enum"][0] if model_prop["enum"] else "") + default = model_prop.get( + "default", model_prop["enum"][0] if model_prop["enum"] else "" + ) del model_prop["enum"] model_prop["default"] = default @@ -165,38 +167,36 @@ def convert_to_freetext(schema: dict) -> dict: def main(): - parser = argparse.ArgumentParser( - description="Manage models in adapter JSON schemas" - ) + parser = argparse.ArgumentParser(description="Manage models in adapter JSON schemas") parser.add_argument( "--adapter", required=True, choices=["llm", "embedding"], - help="Adapter type (llm or embedding)" + help="Adapter type (llm or embedding)", ) parser.add_argument( - "--provider", - required=True, - help="Provider name (e.g., 'openai', 'anthropic')" + "--provider", required=True, help="Provider name (e.g., 'openai', 'anthropic')" ) parser.add_argument( "--action", required=True, - choices=["list", "add-enum", "remove-enum", "set-default", "update-description", "to-enum", "to-freetext"], - help="Action to perform" - ) - parser.add_argument( - "--models", - help="Comma-separated list of models (for add/remove/set-default)" + choices=[ + "list", + "add-enum", + "remove-enum", + "set-default", + "update-description", + "to-enum", + "to-freetext", + ], + help="Action to perform", ) parser.add_argument( - "--description", - help="New description for model field" + "--models", help="Comma-separated list of models (for add/remove/set-default)" ) + parser.add_argument("--description", help="New description for model field") parser.add_argument( - "--dry-run", - action="store_true", - help="Show changes without applying them" + "--dry-run", action="store_true", help="Show changes without applying them" ) args = parser.parse_args() @@ -218,11 +218,11 @@ def main(): print(f"Model configuration for {args.provider} {args.adapter}:") print(f" Type: {info['type']}") print(f" Default: {info['default']}") - if info['enum']: + if info["enum"]: print(f" Enum values: {', '.join(info['enum'])}") else: print(" Enum: (free text)") - if info['description']: + if info["description"]: print(f" Description: {info['description']}") return 0 @@ -255,7 +255,7 @@ def main(): print("Error: --description required for update-description action") return 1 schema = update_description(schema, args.description) - print(f"Updated description") + print("Updated description") elif args.action == "to-enum": schema = convert_to_enum(schema) diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/azure_ai_foundry.py b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/azure_ai_foundry.py index 2a37a5e007..4bb234077e 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/azure_ai_foundry.py +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/azure_ai_foundry.py @@ -1,6 +1,6 @@ from typing import Any -from unstract.sdk1.adapters.base1 import BaseAdapter, AzureAIFoundryLLMParameters +from unstract.sdk1.adapters.base1 import AzureAIFoundryLLMParameters, BaseAdapter from unstract.sdk1.adapters.enums import AdapterTypes From 98888be79c101aff20caedd46fade6bc65dd2cba Mon Sep 17 00:00:00 2001 From: Hari John Kuriakose Date: Tue, 23 Dec 2025 11:16:00 +0530 Subject: [PATCH 4/5] * feat: rename adapter-ops skill * remove claude artifacts from gitignore --- .../skills/{unstract-adapter-extension => adapter-ops}/SKILL.md | 2 +- .../assets/templates/embedding_adapter.py.template | 0 .../assets/templates/embedding_parameters.py.template | 0 .../assets/templates/embedding_schema.json.template | 0 .../assets/templates/llm_adapter.py.template | 0 .../assets/templates/llm_parameters.py.template | 0 .../assets/templates/llm_schema.json.template | 0 .../references/adapter_patterns.md | 0 .../references/json_schema_guide.md | 0 .../references/provider_capabilities.md | 0 .../scripts/check_adapter_updates.py | 0 .../scripts/init_embedding_adapter.py | 0 .../scripts/init_llm_adapter.py | 0 .../scripts/manage_models.py | 0 .gitignore | 2 -- 15 files changed, 1 insertion(+), 3 deletions(-) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/SKILL.md (99%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/assets/templates/embedding_adapter.py.template (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/assets/templates/embedding_parameters.py.template (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/assets/templates/embedding_schema.json.template (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/assets/templates/llm_adapter.py.template (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/assets/templates/llm_parameters.py.template (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/assets/templates/llm_schema.json.template (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/references/adapter_patterns.md (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/references/json_schema_guide.md (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/references/provider_capabilities.md (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/scripts/check_adapter_updates.py (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/scripts/init_embedding_adapter.py (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/scripts/init_llm_adapter.py (100%) rename .claude/skills/{unstract-adapter-extension => adapter-ops}/scripts/manage_models.py (100%) diff --git a/.claude/skills/unstract-adapter-extension/SKILL.md b/.claude/skills/adapter-ops/SKILL.md similarity index 99% rename from .claude/skills/unstract-adapter-extension/SKILL.md rename to .claude/skills/adapter-ops/SKILL.md index 3f91f2e4f3..755418469e 100644 --- a/.claude/skills/unstract-adapter-extension/SKILL.md +++ b/.claude/skills/adapter-ops/SKILL.md @@ -1,5 +1,5 @@ --- -name: unstract-adapter-extension +name: adapter-ops description: Extend LLM and embedding adapters in unstract/sdk1. Use when adding new adapters (LLM or embedding), removing adapters, adding/removing models to existing adapters, or editing adapter configurations. Supports OpenAI-compatible providers, cloud providers (AWS Bedrock, VertexAI, Azure), and self-hosted models (Ollama). --- diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/embedding_adapter.py.template b/.claude/skills/adapter-ops/assets/templates/embedding_adapter.py.template similarity index 100% rename from .claude/skills/unstract-adapter-extension/assets/templates/embedding_adapter.py.template rename to .claude/skills/adapter-ops/assets/templates/embedding_adapter.py.template diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/embedding_parameters.py.template b/.claude/skills/adapter-ops/assets/templates/embedding_parameters.py.template similarity index 100% rename from .claude/skills/unstract-adapter-extension/assets/templates/embedding_parameters.py.template rename to .claude/skills/adapter-ops/assets/templates/embedding_parameters.py.template diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/embedding_schema.json.template b/.claude/skills/adapter-ops/assets/templates/embedding_schema.json.template similarity index 100% rename from .claude/skills/unstract-adapter-extension/assets/templates/embedding_schema.json.template rename to .claude/skills/adapter-ops/assets/templates/embedding_schema.json.template diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/llm_adapter.py.template b/.claude/skills/adapter-ops/assets/templates/llm_adapter.py.template similarity index 100% rename from .claude/skills/unstract-adapter-extension/assets/templates/llm_adapter.py.template rename to .claude/skills/adapter-ops/assets/templates/llm_adapter.py.template diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/llm_parameters.py.template b/.claude/skills/adapter-ops/assets/templates/llm_parameters.py.template similarity index 100% rename from .claude/skills/unstract-adapter-extension/assets/templates/llm_parameters.py.template rename to .claude/skills/adapter-ops/assets/templates/llm_parameters.py.template diff --git a/.claude/skills/unstract-adapter-extension/assets/templates/llm_schema.json.template b/.claude/skills/adapter-ops/assets/templates/llm_schema.json.template similarity index 100% rename from .claude/skills/unstract-adapter-extension/assets/templates/llm_schema.json.template rename to .claude/skills/adapter-ops/assets/templates/llm_schema.json.template diff --git a/.claude/skills/unstract-adapter-extension/references/adapter_patterns.md b/.claude/skills/adapter-ops/references/adapter_patterns.md similarity index 100% rename from .claude/skills/unstract-adapter-extension/references/adapter_patterns.md rename to .claude/skills/adapter-ops/references/adapter_patterns.md diff --git a/.claude/skills/unstract-adapter-extension/references/json_schema_guide.md b/.claude/skills/adapter-ops/references/json_schema_guide.md similarity index 100% rename from .claude/skills/unstract-adapter-extension/references/json_schema_guide.md rename to .claude/skills/adapter-ops/references/json_schema_guide.md diff --git a/.claude/skills/unstract-adapter-extension/references/provider_capabilities.md b/.claude/skills/adapter-ops/references/provider_capabilities.md similarity index 100% rename from .claude/skills/unstract-adapter-extension/references/provider_capabilities.md rename to .claude/skills/adapter-ops/references/provider_capabilities.md diff --git a/.claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py b/.claude/skills/adapter-ops/scripts/check_adapter_updates.py similarity index 100% rename from .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py rename to .claude/skills/adapter-ops/scripts/check_adapter_updates.py diff --git a/.claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py b/.claude/skills/adapter-ops/scripts/init_embedding_adapter.py similarity index 100% rename from .claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py rename to .claude/skills/adapter-ops/scripts/init_embedding_adapter.py diff --git a/.claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py b/.claude/skills/adapter-ops/scripts/init_llm_adapter.py similarity index 100% rename from .claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py rename to .claude/skills/adapter-ops/scripts/init_llm_adapter.py diff --git a/.claude/skills/unstract-adapter-extension/scripts/manage_models.py b/.claude/skills/adapter-ops/scripts/manage_models.py similarity index 100% rename from .claude/skills/unstract-adapter-extension/scripts/manage_models.py rename to .claude/skills/adapter-ops/scripts/manage_models.py diff --git a/.gitignore b/.gitignore index 80c568a0f4..7addcab6a8 100644 --- a/.gitignore +++ b/.gitignore @@ -686,8 +686,6 @@ prompting/ .prompting/ # Claude -CLAUDE.md -.claude/* CONTRIBUTION_GUIDE.md .mcp.json From 6ff7b0516ad4020e29b6629e70bbfb56a567c3ee Mon Sep 17 00:00:00 2001 From: Hari John Kuriakose Date: Tue, 23 Dec 2025 11:28:45 +0530 Subject: [PATCH 5/5] chore: marked skill script as executable --- .claude/skills/adapter-ops/scripts/check_adapter_updates.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .claude/skills/adapter-ops/scripts/check_adapter_updates.py diff --git a/.claude/skills/adapter-ops/scripts/check_adapter_updates.py b/.claude/skills/adapter-ops/scripts/check_adapter_updates.py old mode 100644 new mode 100755