diff --git a/autogen/structure/models/__init__.py b/autogen/structure/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/autogen/structure/models/field_model.py b/autogen/structure/models/field_model.py new file mode 100644 index 0000000000..5a6e645221 --- /dev/null +++ b/autogen/structure/models/field_model.py @@ -0,0 +1,116 @@ +# copied from https://github.com/lion-agi/lion-os/blob/main/lion/core/models/field_model.py +# copyright by HaiyangLi, APACHE LICENSE 2.0 + +from typing import Any, Callable + +from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic.fields import FieldInfo +from pydantic_core import PydanticUndefined + + +class FieldModel(BaseModel): + """Model for defining and managing field definitions. + + Provides a structured way to define fields with: + - Type annotations and validation + - Default values and factories + - Documentation and metadata + - Serialization options + + Example: + ```python + field = FieldModel( + name="age", + annotation=int, + default=0, + description="User age in years", + validator=lambda v: v if v >= 0 else 0 + ) + ``` + + Attributes: + default: Default field value + default_factory: Function to generate default value + title: Field title for documentation + description: Field description + examples: Example values + validators: Validation functions + exclude: Exclude from serialization + deprecated: Mark as deprecated + frozen: Mark as immutable + alias: Alternative field name + alias_priority: Priority for alias resolution + name: Field name (required) + annotation: Type annotation + validator: Validation function + validator_kwargs: Validator parameters + + Notes: + - All attributes except 'name' can be UNDEFINED + - validator_kwargs are passed to field_validator decorator + - Cannot have both default and default_factory + """ + + model_config = ConfigDict( + extra="allow", + validate_default=False, + arbitrary_types_allowed=True, + use_enum_values=True, + ) + + # Field configuration attributes + default: Any = PydanticUndefined # Default value + default_factory: Callable = PydanticUndefined # Factory function for default value + title: str = PydanticUndefined # Field title + description: str = PydanticUndefined # Field description + examples: list = PydanticUndefined # Example values + validators: list = PydanticUndefined # Validation functions + exclude: bool = PydanticUndefined # Exclude from serialization + deprecated: bool = PydanticUndefined # Mark as deprecated + frozen: bool = PydanticUndefined # Mark as immutable + alias: str = PydanticUndefined # Alternative field name + alias_priority: int = PydanticUndefined # Priority for alias resolution + + # Core field attributes + name: str = Field(..., exclude=True) # Field name (required) + annotation: type | Any = Field(PydanticUndefined, exclude=True) # Type annotation + validator: Callable | Any = Field(PydanticUndefined, exclude=True) # Validation function + validator_kwargs: dict | Any = Field(default_factory=dict, exclude=True) # Validator parameters + + @property + def field_info(self) -> FieldInfo: + """Generate Pydantic FieldInfo object from field configuration. + + Returns: + FieldInfo object with all configured attributes + + Notes: + - Uses clean dict to exclude UNDEFINED values + - Sets annotation to Any if not specified + - Preserves all metadata in field_info + """ + annotation = self.annotation if self.annotation is not PydanticUndefined else Any + field_obj: FieldInfo = Field(**self.to_dict(True)) # type: ignore + field_obj.annotation = annotation + return field_obj + + @property + def field_validator(self) -> dict[str, Callable] | None: + """Generate field validator configuration. + + Returns: + Dictionary mapping validator name to function, + or None if no validator defined + + Notes: + - Validator name is f"{field_name}_validator" + - Uses validator_kwargs if provided + - Returns None if validator is UNDEFINED + """ + if self.validator is PydanticUndefined: + return None + kwargs = self.validator_kwargs or {} + return {f"{self.name}_validator": field_validator(self.name, **kwargs)(self.validator)} + + +__all__ = ["FieldModel"] diff --git a/autogen/structure/models/instruct.py b/autogen/structure/models/instruct.py new file mode 100644 index 0000000000..0298bb2789 --- /dev/null +++ b/autogen/structure/models/instruct.py @@ -0,0 +1,98 @@ +# copied from https://github.com/lion-agi/lion-os/blob/main/lion/protocols/operatives/instruct.py +# copyright by HaiyangLi, APACHE LICENSE 2.0 + +from typing import Any + +from pydantic import BaseModel, JsonValue, field_validator + +from .field_model import FieldModel +from .prompts import ( + context_examples, + context_field_description, + guidance_examples, + guidance_field_description, + instruction_examples, + instruction_field_description, +) + + +def validate_instruction(cls, value) -> JsonValue | None: + """Validates that the instruction is not empty or None and is in the correct format. + + Args: + cls: The validator class method. + value (JsonValue | None): The instruction value to validate. + + Returns: + JsonValue | None: The validated instruction or None if invalid. + """ + if value is None or (isinstance(value, str) and not value.strip()): + return None + return value + + +# Field Models +INSTRUCTION_FIELD = FieldModel( + name="instruction", + annotation=JsonValue | None, + default=None, + title="Primary Instruction", + description=instruction_field_description, + examples=instruction_examples, + validator=validate_instruction, + validator_kwargs={"mode": "before"}, +) + +GUIDANCE_FIELD = FieldModel( + name="guidance", + annotation=JsonValue | None, + default=None, + title="Execution Guidance", + description=guidance_field_description, + examples=guidance_examples, +) + +CONTEXT_FIELD = FieldModel( + name="context", + annotation=JsonValue | None, + default=None, + title="Task Context", + description=context_field_description, + examples=context_examples, +) + + +class Instruct(BaseModel): + """Model for defining instruction parameters and execution requirements. + + Attributes: + instruction (JsonValue | None): The primary instruction. + guidance (JsonValue | None): Execution guidance. + context (JsonValue | None): Task context. + reason (bool): Whether to include reasoning. + actions (bool): Whether specific actions are required. + """ + + instruction: JsonValue | None = INSTRUCTION_FIELD.field_info + guidance: JsonValue | None = GUIDANCE_FIELD.field_info + context: JsonValue | None = CONTEXT_FIELD.field_info + + @field_validator("instruction", **INSTRUCTION_FIELD.validator_kwargs) + def _validate_instruction(cls, v): + """Field validator for the 'instruction' field. + + Args: + v: The value to validate. + + Returns: + JsonValue | None: The validated instruction value. + """ + return INSTRUCTION_FIELD.validator(cls, v) + + +class InstructResponse(BaseModel): + instruct: Instruct + response: Any = None + + +__all__ = ["Instruct", "InstructResponse"] diff --git a/autogen/structure/models/new_model_params.py b/autogen/structure/models/new_model_params.py new file mode 100644 index 0000000000..1dbdd5a382 --- /dev/null +++ b/autogen/structure/models/new_model_params.py @@ -0,0 +1,170 @@ +# copied from https://github.com/lion-agi/lion-os/blob/main/lion/core/models/new_model_params.py +# copyright by HaiyangLi, APACHE LICENSE 2.0 + + +import inspect +from typing import Callable + +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, create_model, field_validator, model_validator +from pydantic.fields import FieldInfo + +from .field_model import FieldModel + + +class NewModelParams(BaseModel): + """Configuration class for dynamically creating new Pydantic models.""" + + model_config = ConfigDict( + extra="forbid", + arbitrary_types_allowed=True, + use_enum_values=True, + ) + + name: str | None = None + parameter_fields: dict[str, FieldInfo] = Field(default_factory=dict) + base_type: type[BaseModel] = Field(default=BaseModel) + field_models: list[FieldModel] = Field(default_factory=list) + exclude_fields: list = Field(default_factory=list) + field_descriptions: dict = Field(default_factory=dict) + inherit_base: bool = Field(default=True) + config_dict: dict | None = Field(default=None) + doc: str | None = Field(default=None) + frozen: bool = False + _validators: dict[str, Callable] | None = PrivateAttr(default=None) + _use_keys: set[str] = PrivateAttr(default_factory=set) + + @property + def use_fields(self): + """Get field definitions to use in new model.""" + params = {k: v for k, v in self.parameter_fields.items() if k in self._use_keys} + params.update({f.name: f.field_info for f in self.field_models if f.name in self._use_keys}) + return {k: (v.annotation, v) for k, v in params.items()} + + @field_validator("parameter_fields", mode="before") + def validate_parameters(cls, value): + """Validate parameter field definitions.""" + if value is None: + return {} + if not isinstance(value, dict): + raise ValueError("Fields must be a dictionary.") + for k, v in value.items(): + if not isinstance(k, str): + raise ValueError("Field names must be strings.") + if not isinstance(v, FieldInfo): + raise ValueError("Field values must be FieldInfo objects.") + return value.copy() + + @field_validator("base_type", mode="before") + def validate_base(cls, value) -> type[BaseModel]: + """Validate base model type.""" + if value is None: + return BaseModel + if isinstance(value, type) and issubclass(value, BaseModel): + return value + if isinstance(value, BaseModel): + return value.__class__ + raise ValueError("Base must be a BaseModel subclass or instance.") + + @field_validator("exclude_fields", mode="before") + def validate_fields(cls, value) -> list[str]: + """Validate excluded fields list.""" + if value is None: + return [] + if isinstance(value, dict): + value = list(value.keys()) + if isinstance(value, set | tuple): + value = list(value) + if isinstance(value, list): + if not all(isinstance(i, str) for i in value): + raise ValueError("Field names must be strings.") + return value.copy() + raise ValueError("Fields must be a list, set, or dictionary.") + + @field_validator("field_descriptions", mode="before") + def validate_field_descriptions(cls, value) -> dict[str, str]: + """Validate field descriptions dictionary.""" + if value is None: + return {} + if not isinstance(value, dict): + raise ValueError("Field descriptions must be a dictionary.") + for k, v in value.items(): + if not isinstance(k, str): + raise ValueError("Field names must be strings.") + if not isinstance(v, str): + raise ValueError("Field descriptions must be strings.") + return value + + @field_validator("name", mode="before") + def validate_name(cls, value) -> str: + """Validate model name.""" + if value is None: + return "StepModel" + if not isinstance(value, str): + raise ValueError("Name must be a string.") + return value + + @field_validator("field_models", mode="before") + def _validate_field_models(cls, value): + """Validate field model definitions.""" + if value is None: + return [] + value = [value] if not isinstance(value, list) else value + if not all(isinstance(i, FieldModel) for i in value): + raise ValueError("Field models must be FieldModel objects.") + return value + + @model_validator(mode="after") + def validate_param_model(self): + """Validate complete model configuration.""" + if self.base_type is not None: + self.parameter_fields.update(self.base_type.model_fields) + + self.parameter_fields.update({f.name: f.field_info for f in self.field_models}) + + use_keys = list(self.parameter_fields.keys()) + use_keys.extend(list(self._use_keys)) + + if self.exclude_fields: + use_keys = [i for i in use_keys if i not in self.exclude_fields] + + self._use_keys = set(use_keys) + + validators = {} + + for i in self.field_models: + if i.field_validator is not None: + validators.update(i.field_validator) + self._validators = validators + + if self.field_descriptions: + for i in self.field_models: + if i.name in self.field_descriptions: + i.description = self.field_descriptions[i.name] + + if not isinstance(self.name, str): + if hasattr(self.base_type, "class_name"): + if callable(self.base_type.class_name): + self.name = self.base_type.class_name() + else: + self.name = self.base_type.class_name + elif inspect.isclass(self.base_type): + self.name = self.base_type.__name__ + + return self + + def create_new_model(self) -> type[BaseModel]: + """Create new Pydantic model with specified configuration.""" + a: type[BaseModel] = create_model( + self.name, + __config__=self.config_dict, + __doc__=self.doc, + __base__=self.base_type if self.inherit_base else None, + __validators__=self._validators, + **self.use_fields, + ) + if self.frozen: + a.model_config["frozen"] = True + return a + + +__all__ = ["NewModelParams"] diff --git a/autogen/structure/models/operative.py b/autogen/structure/models/operative.py new file mode 100644 index 0000000000..d874da4d8f --- /dev/null +++ b/autogen/structure/models/operative.py @@ -0,0 +1,155 @@ +# copied from https://github.com/lion-agi/lion-os/blob/main/lion/protocols/operatives/operative.py +# copyright by HaiyangLi, APACHE LICENSE 2.0 + +from pydantic import BaseModel, Field, PrivateAttr, model_validator +from pydantic.fields import FieldInfo + +from autogen.structure.utils import to_json, validate_keys + +from .field_model import FieldModel +from .new_model_params import NewModelParams + + +class Operative(BaseModel): + """Class representing an operative that handles request and response models for operations.""" + + name: str | None = None + + request_params: NewModelParams | None = Field(default=None) + request_type: type[BaseModel] | None = Field(default=None) + + response_params: NewModelParams | None = Field(default=None) + response_type: type[BaseModel] | None = Field(default=None) + response_model: BaseModel | None = Field(default=None) + response_str_dict: dict | str | None = Field(default=None) + + auto_retry_parse: bool = True + max_retries: int = 3 + _should_retry: bool = PrivateAttr(default=None) + + @model_validator(mode="after") + def _validate(self): + """Validates the operative instance after initialization.""" + if self.request_type is None: + self.request_type = self.request_params.create_new_model() + if self.name is None: + self.name = self.request_params.name or self.request_type.__name__ + return self + + def raise_validate_pydantic(self, text: str) -> None: + """Validates and updates the response model using strict matching. + + Args: + text (str): The text to validate and parse into the response model. + + Raises: + Exception: If the validation fails. + """ + d_ = to_json(text, fuzzy_parse=True) + if isinstance(d_, list | tuple) and len(d_) == 1: + d_ = d_[0] + try: + d_ = validate_keys(d_, self.request_type.model_fields, handle_unmatched="raise") + self.response_model = self.request_type.model_validate(d_) + self._should_retry = False + except Exception: + self.response_str_dict = d_ + self._should_retry = True + + def force_validate_pydantic(self, text: str): + """Forcibly validates and updates the response model, allowing unmatched fields. + + Args: + text (str): The text to validate and parse into the response model. + """ + d_ = text + try: + d_ = to_json(text, fuzzy_parse=True) + if isinstance(d_, list | tuple) and len(d_) == 1: + d_ = d_[0] + d_ = validate_keys(d_, self.request_type.model_fields, handle_unmatched="force") + self.response_model = self.request_type.model_validate(d_) + self._should_retry = False + except Exception: + self.response_str_dict = d_ + self.response_model = None + self._should_retry = True + + def update_response_model(self, text: str | None = None, data: dict | None = None) -> BaseModel | dict | str | None: + """Updates the response model based on the provided text or data. + + Args: + text (str, optional): The text to parse and validate. + data (dict, optional): The data to update the response model with. + + Returns: + BaseModel | dict | str | None: The updated response model or raw data. + + Raises: + ValueError: If neither text nor data is provided. + """ + if text is None and data is None: + raise ValueError("Either text or data must be provided.") + + if text: + self.response_str_dict = text + try: + self.raise_validate_pydantic(text) + except Exception: + self.force_validate_pydantic(text) + + if data and self.response_type: + d_ = self.response_model.model_dump() + d_.update(data) + self.response_model = self.response_type.model_validate(d_) + + if not self.response_model and isinstance(self.response_str_dict, list): + try: + self.response_model = [self.request_type.model_validate(d_) for d_ in self.response_str_dict] + except Exception: + pass + + return self.response_model or self.response_str_dict + + def create_response_type( + self, + response_params: NewModelParams | None = None, + field_models: list[FieldModel] | None = None, + parameter_fields: dict[str, FieldInfo] | None = None, + exclude_fields: list[str] | None = None, + field_descriptions: dict[str, str] | None = None, + inherit_base: bool = True, + config_dict: dict | None = None, + doc: str | None = None, + frozen: bool = False, + validators: dict | None = None, + ) -> None: + """Creates a new response type based on the provided parameters. + + Args: + response_params (NewModelParams, optional): Parameters for the new response model. + field_models (list[FieldModel], optional): List of field models. + parameter_fields (dict[str, FieldInfo], optional): Dictionary of parameter fields. + exclude_fields (list, optional): List of fields to exclude. + field_descriptions (dict, optional): Dictionary of field descriptions. + inherit_base (bool, optional): Whether to inherit the base model. + config_dict (dict | None, optional): Configuration dictionary. + doc (str | None, optional): Documentation string. + frozen (bool, optional): Whether the model is frozen. + validators (dict, optional): Dictionary of validators. + """ + self.response_params = response_params or NewModelParams( + parameter_fields=parameter_fields, + field_models=field_models, + exclude_fields=exclude_fields, + field_descriptions=field_descriptions, + inherit_base=inherit_base, + config_dict=config_dict, + doc=doc, + frozen=frozen, + base_type=self.request_params.base_type, + ) + if validators and isinstance(validators, dict): + self.response_params._validators.update(validators) + + self.response_type = self.response_params.create_new_model() diff --git a/autogen/structure/models/prompts.py b/autogen/structure/models/prompts.py new file mode 100644 index 0000000000..586cadbf65 --- /dev/null +++ b/autogen/structure/models/prompts.py @@ -0,0 +1,81 @@ +# copied from https://github.com/lion-agi/lion-os/blob/main/lion/protocols/operatives/prompts.py +# copyright by HaiyangLi, APACHE LICENSE 2.0 + +from pydantic import JsonValue + +instruction_field_description = ( + "Define the core task or instruction to be executed. Your instruction should:\n\n" + "1. Be specific and actionable\n" + "2. Clearly state the expected outcome\n" + "3. Include any critical constraints or requirements\n\n" + "**Guidelines for writing effective instructions:**\n" + "- Start with a clear action verb (e.g., analyze, create, evaluate)\n" + "- Specify the scope and boundaries of the task\n" + "- Include success criteria when applicable\n" + "- Break complex tasks into distinct steps\n\n" + "**Examples:**\n" + "- 'Analyze the provided sales data and identify top 3 performing products'\n" + "- 'Generate a Python function that validates email addresses'\n" + "- 'Create a data visualization showing monthly revenue trends'" +) + +guidance_field_description = ( + "Provide strategic direction and constraints for task execution.\n\n" + "**Key components to include:**\n" + "1. Methodological preferences\n" + "2. Quality standards and requirements\n" + "3. Specific limitations or boundaries\n" + "4. Performance expectations\n\n" + "**Best practices:**\n" + "- Be explicit about any assumptions that should be made\n" + "- Specify preferred approaches or techniques\n" + "- Detail any constraints on resources or methods\n" + "- Include relevant standards or compliance requirements\n\n" + "Leave as None if no specific guidance is needed beyond the instruction." +) + +context_field_description = ( + "Supply essential background information and current state data required for " + "task execution.\n\n" + "**Include relevant details about:**\n" + "1. Environmental conditions\n" + "2. Historical context\n" + "3. Related systems or processes\n" + "4. Previous outcomes or decisions\n\n" + "**Context should:**\n" + "- Be directly relevant to the task\n" + "- Provide necessary background without excess detail\n" + "- Include any dependencies or prerequisites\n" + "- Specify the current state of the system\n\n" + "Set to None if no additional context is required." +) + + +# Example structures for each field to demonstrate proper formatting +instruction_examples: list[JsonValue] = [ + "Analyze the dataset 'sales_2023.csv' and identify revenue trends", + "Create a Python function to process customer feedback data", + { + "task": "data_analysis", + "target": "sales_performance", + "scope": ["revenue", "growth", "seasonality"], + }, +] + +guidance_examples: list[JsonValue] = [ + "Use statistical methods for trend analysis", + "Optimize for readability and maintainability", + { + "methods": ["regression", "time_series"], + "constraints": {"memory": "2GB", "time": "5min"}, + }, +] + +context_examples: list[JsonValue] = [ + "Previous analysis showed seasonal patterns", + { + "prior_results": {"accuracy": 0.95}, + "system_state": "production", + "dependencies": ["numpy", "pandas"], + }, +]