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 FieldModel and ModelParams)

Below is an overview of the major components in this subsystem.

1. FieldModel (field_model.py)

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)

Collects configuration for generating a new Pydantic model class, such as:

  • parameter_fields: a dictionary mapping field names to FieldInfo.

  • field_models: a list of FieldModel objects.

  • exclude_fields: a list of fields to remove from the final model.

  • inherit_base: Whether to extend a base model class (default: True).

  • config_dict: Pydantic config overrides (e.g., frozen=True).

  • doc: Docstring for the generated model.

Finally, call 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)

This is a Pydantic model that allows runtime modifications to its schema, including:

  • add_field(name, value=..., annotation=...)(): Add a new field.

  • update_field(...)(): Update an existing field or create if absent.

  • remove_field(name)(): Remove a field.

All extra fields are stored in extra_fields (mapping from name to pydantic.FieldInfo) and extra_field_models (mapping from name to 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)

A specialized object for managing nested dictionary data:

  • get(indices, default)(), set(indices, value)(), pop(indices)(), etc. for deeply nested access or updates.

  • update(indices, value)() merges with an existing dict or appends to a list.

  • 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)

A lightweight extension of 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)

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 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 ModelParams (for advanced code generation scenarios).

  4. Store nested data in 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.