# 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, StrictStr, field_validator
from typing import Any, ClassVar, Dict, List, Optional, Union
from typing_extensions import Annotated
from mixpeek.models.learning_algorithm import LearningAlgorithm
from typing import Optional, Set
from typing_extensions import Self

class LearnedFusionConfig(BaseModel):
    """
    Configuration for learned fusion with bandit-based weight optimization.  Enables personalized feature weighting by learning from user interactions. The system learns which features (text, image, audio, etc.) matter most for different users or contexts.  ┌─────────────────────────────────────────────────────────────────────────────┐ │ HOW LEARNED FUSION WORKS                                                     │ ├─────────────────────────────────────────────────────────────────────────────┤ │ 1. Query arrives with context (user_id, segment, etc.)                      │ │ 2. Look up bandit state for this context                                    │ │ 3. Sample feature weights from Beta distributions                           │ │ 4. Execute separate queries per feature with learned weights                │ │ 5. Fuse results using sampled weights                                       │ │ 6. On feedback (click), update bandit for relevant features                 │ └─────────────────────────────────────────────────────────────────────────────┘  Cold Start Handling:     - NEW users: Uses demographic context (user_segment, device_type)     - Returning users: Uses personal context after min_interactions     - Fallback: Global context as ultimate fallback  Requirements:     - Interactions API must be called with feedback (clicks, etc.)     - Context features should be passed in query inputs  Example:     ```python     LearnedFusionConfig(         algorithm=LearningAlgorithm.THOMPSON_SAMPLING,         context_features=[\"INPUT.user_id\"],         demographic_features=[\"INPUT.user_segment\", \"INPUT.device_type\"],         reward_signal=\"click\",         min_interactions=5,     )     ```
    """ # noqa: E501
    algorithm: Optional[LearningAlgorithm] = Field(default=None, description="Learning algorithm for weight optimization. THOMPSON_SAMPLING (default): Beta-Bernoulli bandit with natural exploration. Works immediately, no tuning needed, best for most use cases.")
    context_features: Optional[List[StrictStr]] = Field(default=None, description="Template variables for personal context (e.g., ['INPUT.user_id']). Empty list = global learning (same weights for all users). Personal context is used after user has min_interactions. Supports: INPUT.*, CONTEXT.*, etc.")
    demographic_features: Optional[List[StrictStr]] = Field(default=None, description="Template variables for demographic fallback context. Used for NEW users with < min_interactions. Enables cold start by learning from similar user segments. Examples: INPUT.user_segment, INPUT.device_type, INPUT.country.")
    fallback_strategy: Optional[StrictStr] = Field(default='hierarchical', description="Strategy when user lacks sufficient history. 'hierarchical': Try personal → demographic → global (recommended). 'global': Skip demographic, fall back directly to global.")
    min_interactions: Optional[Annotated[int, Field(le=100, strict=True, ge=1)]] = Field(default=5, description="Minimum interactions before using personal context. Below this threshold, uses demographic or global context. Prevents overfitting to small samples. Typical range: 3-10.")
    reward_signal: Optional[StrictStr] = Field(default='click', description="Interaction type(s) that count as positive reward. Single: 'click', 'purchase', 'positive_feedback'. Multiple: 'click,purchase' (comma-separated). Determines when to strengthen feature weights.")
    exploration_bonus: Optional[Union[Annotated[float, Field(le=10.0, strict=True, ge=0.1)], Annotated[int, Field(le=10, strict=True, ge=1)]]] = Field(default=1.0, description="Controls exploration vs exploitation balance. 1.0 = balanced (default). Higher = more exploration (try diverse weights). Lower = more exploitation (use known winners). Typical range: 0.5-2.0.")
    prior_alpha: Optional[Union[Annotated[float, Field(strict=True, ge=0.1)], Annotated[int, Field(strict=True, ge=1)]]] = Field(default=1.0, description="Beta distribution prior for positive feedback (α). 1.0 = uniform prior (no initial bias). Higher = initial belief that features are effective.")
    prior_beta: Optional[Union[Annotated[float, Field(strict=True, ge=0.1)], Annotated[int, Field(strict=True, ge=1)]]] = Field(default=1.0, description="Beta distribution prior for negative feedback (β). 1.0 = uniform prior (no initial bias). Higher = initial belief that features are ineffective.")
    __properties: ClassVar[List[str]] = ["algorithm", "context_features", "demographic_features", "fallback_strategy", "min_interactions", "reward_signal", "exploration_bonus", "prior_alpha", "prior_beta"]

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

        if value not in set(['hierarchical', 'global']):
            raise ValueError("must be one of enum values ('hierarchical', 'global')")
        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 LearnedFusionConfig 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 LearnedFusionConfig 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"),
            "context_features": obj.get("context_features"),
            "demographic_features": obj.get("demographic_features"),
            "fallback_strategy": obj.get("fallback_strategy") if obj.get("fallback_strategy") is not None else 'hierarchical',
            "min_interactions": obj.get("min_interactions") if obj.get("min_interactions") is not None else 5,
            "reward_signal": obj.get("reward_signal") if obj.get("reward_signal") is not None else 'click',
            "exploration_bonus": obj.get("exploration_bonus") if obj.get("exploration_bonus") is not None else 1.0,
            "prior_alpha": obj.get("prior_alpha") if obj.get("prior_alpha") is not None else 1.0,
            "prior_beta": obj.get("prior_beta") if obj.get("prior_beta") is not None else 1.0
        })
        return _obj


