======================================= Model System ======================================= This module provides foundational classes and utilities for **dynamically** creating and managing data models within LionAGI. These “operatives” form the building blocks of the system, offering features like: - **Runtime field creation** (add, update, remove fields) - **Nested validation** via Pydantic - **Hashing/serialization** for easy storage and transport - **Schema generation** (via :class:`FieldModel` and :class:`ModelParams`) Below is an overview of the major components in this subsystem. ---------------------------- 1. FieldModel (``field_model.py``) ---------------------------- .. module:: lionagi.operatives.models.field_model :synopsis: Structured definition of Pydantic fields. .. class:: FieldModel :extends: SchemaModel A configurable **field definition** that captures: - **Name** (str) - **Annotation** (type or Any) - **Default** or **default_factory** - Optional **validator** (a function) - Documentation info (title, description, examples) - Additional Pydantic flags (e.g. ``exclude``, ``deprecated``, etc.) **Usage**:: from lionagi.operatives.models.field_model import FieldModel field = FieldModel( name="age", annotation=int, default=0, description="User age in years", validator=lambda cls, v: v if v >= 0 else 0 ) # Then use field.field_info or field.field_validator as needed ---------------------------- 2. ModelParams (``model_params.py``) ---------------------------- .. module:: lionagi.operatives.models.model_params :synopsis: Dynamically create new Pydantic models. .. class:: ModelParams :extends: SchemaModel Collects **configuration** for generating a new Pydantic model class, such as: - :attr:`parameter_fields`: a dictionary mapping field names to :class:`FieldInfo`. - :attr:`field_models`: a list of :class:`FieldModel` objects. - :attr:`exclude_fields`: a list of fields to remove from the final model. - :attr:`inherit_base`: Whether to extend a base model class (default: True). - :attr:`config_dict`: Pydantic config overrides (e.g., ``frozen=True``). - :attr:`doc`: Docstring for the generated model. Finally, call :meth:`create_new_model()` to get a brand-new Pydantic class (with your specified fields, validators, docstring, etc.). **Example**:: from pydantic import BaseModel from lionagi.operatives.models.model_params import ModelParams from lionagi.operatives.models.field_model import FieldModel params = ModelParams( name="DynamicUser", base_type=BaseModel, field_models=[ FieldModel(name="username", annotation=str, default="guest"), FieldModel(name="age", annotation=int, default=0), ], doc="Dynamically created user model." ) DynamicUser = params.create_new_model() user = DynamicUser(username="Alice", age=30) print(user) # => DynamicUser(username='Alice', age=30) --------------------------- 3. OperableModel (``operable_model.py``) --------------------------- .. module:: lionagi.operatives.models.operable_model :synopsis: Extends Pydantic for dynamic field management. .. class:: OperableModel :extends: HashableModel This is a Pydantic model that allows **runtime** modifications to its schema, including: - :meth:`add_field(name, value=..., annotation=...)`: Add a new field. - :meth:`update_field(...)`: Update an existing field or create if absent. - :meth:`remove_field(name)`: Remove a field. All **extra fields** are stored in :attr:`extra_fields` (mapping from name to :class:`pydantic.FieldInfo`) and :attr:`extra_field_models` (mapping from name to :class:`FieldModel`). The underlying dictionary structure remains valid with Pydantic's type checks and serialization logic. **Example**:: from lionagi.operatives.models.operable_model import OperableModel class User(OperableModel): name: str = "default_name" user = User() user.add_field("age", value=25, annotation=int) print(user.age) # => 25 user.update_field("age", value=26) print(user.age) # => 26 user.remove_field("age") -------------------------- 4. Note (``note.py``) -------------------------- .. module:: lionagi.operatives.models.note :synopsis: A flexible container for nested data. .. class:: Note :extends: pydantic.BaseModel A specialized object for **managing nested dictionary data**: - :meth:`get(indices, default)`, :meth:`set(indices, value)`, :meth:`pop(indices)`, etc. for deeply nested access or updates. - :meth:`update(indices, value)` merges with an existing dict or appends to a list. - :meth:`keys(flat=True|False)`: Optionally flatten nested structures. It's a convenient alternative to constantly handling deeply nested dictionaries manually in your code. **Example**:: from lionagi.operatives.models.note import Note note = Note(user={"name": "John", "settings": {"theme": "dark"}}) name = note.get(["user", "name"]) # "John" note.update(["user", "settings"], {"language": "en"}) print(note.content) # => {"user": {"name": "John", "settings": {"theme": "dark", "language": "en"}}} -------------------------- 5. SchemaModel (``schema_model.py``) -------------------------- .. module:: lionagi.operatives.model.schema_model :synopsis: Base model with restricted config and custom `keys()` method. .. class:: SchemaModel :extends: HashableModel A lightweight extension of :class:`HashableModel` that sets Pydantic config to **forbid** extra fields by default and use enum values. Provides a :method:`keys()` utility that returns the field names defined in the schema. ------------------------- 6. HashableModel (``hashable_model.py``) ------------------------- .. module:: lionagi.utils :synopsis: Adds hashing to Pydantic models. .. class:: HashableModel :extends: pydantic.BaseModel Enables your model to be **hashable**, so it can be used as keys in a dictionary or placed in a set. It does this by converting all fields to a dictionary (via :meth:`to_dict`) and then hashing the sorted key-value pairs. **Note**: Some fields may need to be serialized or converted to strings if they are not inherently hashable. **Example**:: from lionagi.utils import HashableModel class MyConfig(HashableModel): alpha: float beta: str c1 = MyConfig(alpha=1.0, beta="test") c2 = MyConfig(alpha=1.0, beta="test") s = {c1, c2} print(len(s)) # => 1 because c1 and c2 have the same hash --------------------- Putting It All Together --------------------- **Typical use case** for these model classes: 1. **Define** a base model with core fields (like a user or config). 2. **Add** or **update** fields at runtime if the structure is not fixed (e.g., an “Operable” approach for flexible schemas). 3. **Dynamically** create entire new models with :class:`ModelParams` (for advanced code generation scenarios). 4. Store nested data in :class:`Note` objects for iterative or complicated updates. 5. Output or persist model objects as needed; they can be hashed, used as dictionary keys, or automatically **serialized** with LionAGI's adapter system. This design allows building truly “operable” data structures in a **dynamic** environment—where the model schema might evolve during runtime, and you need robust type checking, validation, and hashing to maintain data integrity.