# coding: utf-8

"""
    Mixpeek API

    This is the Mixpeek API, providing access to various endpoints for data processing and retrieval.

    The version of the OpenAPI document: 1.3.0
    Contact: info@mixpeek.com
    Generated by OpenAPI Generator (https://openapi-generator.tech)

    Do not edit the class manually.
"""  # noqa: E501


from __future__ import annotations
import pprint
import re  # noqa: F401
import json

from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, field_validator
from typing import Any, ClassVar, Dict, List, Optional
from typing_extensions import Annotated
from typing import Optional, Set
from typing_extensions import Self

class StageParamsCluster(BaseModel):
    """
    Configuration for clustering documents from previous stage results.  Stage Category: REDUCE  Transformation: N documents → K clusters (where K < N typically)  Purpose: Dynamically clusters documents from the pipeline by their embeddings. Unlike group_by which groups by a pre-existing field, cluster discovers natural groupings in the data based on vector similarity.  Performance: Calls clustering inference service. Fast for typical retriever result sets (10-500 documents). For larger datasets, consider using pre-computed clusters with group_by instead.  When to Use:     - Discover themes/topics in search results     - Group semantically similar documents without pre-existing labels     - Analyze patterns in retrieved content     - \"Find the 3 main themes in these results\"     - Auto-categorize search results  When NOT to Use:     - When documents already have cluster/category labels (use group_by)     - For very large result sets (>1000 docs) - use pre-computed clusters     - When you need exact groupings (clustering is approximate)  Output Modes:     - \"clusters\": Returns K cluster summary documents with member lists     - \"labeled\": Returns original N documents with cluster_label added     - \"representatives\": Returns K representative documents (one per cluster)  Common Pipeline Position: FILTER → cluster (this stage) → ENRICH (summarize clusters)  Examples:     - \"Find 3 themes in 60 ads\" → cluster with n_clusters=3     - \"Group similar products\" → cluster with algorithm=hdbscan (auto K)     - \"Discover topics in articles\" → cluster with representatives output
    """ # noqa: E501
    algorithm: Optional[StrictStr] = Field(default='hdbscan', description="Clustering algorithm to use:  - hdbscan: Auto-determines number of clusters, handles noise (DEFAULT, recommended) - kmeans: Fast, requires n_clusters, spherical clusters - dbscan: Density-based, handles noise, requires eps tuning - agglomerative: Hierarchical, good for nested structures - spectral: Graph-based, good for non-convex clusters - gaussian_mixture: Probabilistic, soft cluster assignments  Recommendation: Use 'hdbscan' for exploratory analysis, 'kmeans' when you know K.")
    n_clusters: Optional[Annotated[int, Field(le=100, strict=True, ge=2)]] = Field(default=null, description="Number of clusters to create. Required for kmeans, spectral, agglomerative, gaussian_mixture. Ignored for hdbscan and dbscan (auto-determined).  If not specified for algorithms that need it, auto-calculated as min(8, N/10).  Typical values: 3-5 for theme discovery, 5-10 for topic modeling, 10-20 for fine-grained categorization.")
    min_cluster_size: Optional[Annotated[int, Field(le=100, strict=True, ge=2)]] = Field(default=5, description="Minimum number of documents to form a cluster (HDBSCAN/DBSCAN only).  Lower values = more clusters, may include noise. Higher values = fewer, denser clusters.  Auto-adjusted for small datasets: min(min_cluster_size, N/3). Typical values: 3-5 for small results, 10-20 for large results.")
    feature_uri: Optional[StrictStr] = Field(default='null', description="Feature URI specifying which embedding to cluster on.  OPTIONAL - if not provided, auto-detects from the upstream feature_search stage. When a feature_search stage runs before cluster, its feature_uri is automatically tracked in the pipeline state and used for clustering.  Use the mixpeek:// URI format:   mixpeek://{extractor}@{version}/{output}  Examples: - 'mixpeek://multimodal_extractor@v1/vertex_multimodal_embedding' - 'mixpeek://text_extractor@v1/multilingual_e5_large_instruct_v1' - 'mixpeek://clip_extractor@v1/image_embedding'  The feature_uri is resolved to the actual embedding field name on the documents (e.g., 'multimodal_extractor_v1_multimodal_embedding').  Only specify explicitly if you want to cluster on a different embedding than the one used in the feature_search stage.")
    output_mode: Optional[StrictStr] = Field(default='clusters', description="How to format the output:  - 'clusters': Returns K cluster documents, each containing:   - cluster_id: Cluster identifier   - member_count: Number of documents in cluster   - members: List of member documents   - centroid: Cluster center vector   Use for: Theme analysis, cluster summaries  - 'labeled': Returns original N documents with added fields:   - cluster_id: Assigned cluster   - cluster_score: Distance to centroid (lower = closer)   Use for: Downstream processing with cluster context  - 'representatives': Returns K documents (one per cluster):   - The document closest to each cluster centroid   Use for: Quick sampling, representative examples")
    include_centroids: Optional[StrictBool] = Field(default=True, description="Whether to include centroid vectors in output. Useful for downstream similarity comparisons or visualization. Set to False to reduce response size.")
    max_members_per_cluster: Optional[Annotated[int, Field(le=500, strict=True, ge=1)]] = Field(default=50, description="Maximum members to include per cluster in 'clusters' output mode. Documents are sorted by distance to centroid (closest first). Use to limit response size for large result sets.")
    __properties: ClassVar[List[str]] = ["algorithm", "n_clusters", "min_cluster_size", "feature_uri", "output_mode", "include_centroids", "max_members_per_cluster"]

    @field_validator('algorithm')
    def algorithm_validate_enum(cls, value):
        """Validates the enum"""
        if value is None:
            return value

        if value not in set(['kmeans', 'hdbscan', 'dbscan', 'agglomerative', 'spectral', 'gaussian_mixture']):
            raise ValueError("must be one of enum values ('kmeans', 'hdbscan', 'dbscan', 'agglomerative', 'spectral', 'gaussian_mixture')")
        return value

    @field_validator('output_mode')
    def output_mode_validate_enum(cls, value):
        """Validates the enum"""
        if value is None:
            return value

        if value not in set(['clusters', 'labeled', 'representatives']):
            raise ValueError("must be one of enum values ('clusters', 'labeled', 'representatives')")
        return value

    model_config = ConfigDict(
        populate_by_name=True,
        validate_assignment=True,
        protected_namespaces=(),
    )


    def to_str(self) -> str:
        """Returns the string representation of the model using alias"""
        return pprint.pformat(self.model_dump(by_alias=True))

    def to_json(self) -> str:
        """Returns the JSON representation of the model using alias"""
        # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
        return json.dumps(self.to_dict())

    @classmethod
    def from_json(cls, json_str: str) -> Optional[Self]:
        """Create an instance of StageParamsCluster from a JSON string"""
        return cls.from_dict(json.loads(json_str))

    def to_dict(self) -> Dict[str, Any]:
        """Return the dictionary representation of the model using alias.

        This has the following differences from calling pydantic's
        `self.model_dump(by_alias=True)`:

        * `None` is only added to the output dict for nullable fields that
          were set at model initialization. Other fields with value `None`
          are ignored.
        """
        excluded_fields: Set[str] = set([
        ])

        _dict = self.model_dump(
            by_alias=True,
            exclude=excluded_fields,
            exclude_none=True,
        )
        return _dict

    @classmethod
    def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
        """Create an instance of StageParamsCluster from a dict"""
        if obj is None:
            return None

        if not isinstance(obj, dict):
            return cls.model_validate(obj)

        _obj = cls.model_validate({
            "algorithm": obj.get("algorithm") if obj.get("algorithm") is not None else 'hdbscan',
            "n_clusters": obj.get("n_clusters") if obj.get("n_clusters") is not None else null,
            "min_cluster_size": obj.get("min_cluster_size") if obj.get("min_cluster_size") is not None else 5,
            "feature_uri": obj.get("feature_uri") if obj.get("feature_uri") is not None else 'null',
            "output_mode": obj.get("output_mode") if obj.get("output_mode") is not None else 'clusters',
            "include_centroids": obj.get("include_centroids") if obj.get("include_centroids") is not None else True,
            "max_members_per_cluster": obj.get("max_members_per_cluster") if obj.get("max_members_per_cluster") is not None else 50
        })
        return _obj


