Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/deepiri_modelkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@

__version__ = "0.1.0"

from .contracts.models import AIModel, AIModelPydantic, ModelInput, ModelOutput, ModelMetadata
from .contracts.models import (
AIModel,
AIModelPydantic,
ModelInput,
ModelOutput,
ModelMetadata,
)
from .contracts.events import (
ModelReadyEvent,
InferenceEvent,
Expand Down Expand Up @@ -33,4 +39,3 @@
"get_error_logger",
"ErrorLogger",
]

9 changes: 6 additions & 3 deletions src/deepiri_modelkit/contracts/contract.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Model contract for registry (separated from models.py to avoid Pydantic Protocol conflicts)
"""

from __future__ import annotations

from typing import Dict, Any, Optional
Expand All @@ -12,16 +13,18 @@
class ModelContract(BaseModel):
"""
Complete model contract for registry.

A contract is serializable metadata that describes a model's interface,
input/output schemas, and validation requirements. It does NOT contain
the actual model instance (which would be a Protocol type that Pydantic
cannot serialize). The model instance should be loaded separately when needed.
"""

metadata: ModelMetadata
input_schema: Dict[str, Any]
output_schema: Dict[str, Any]
validation_tests: Optional[list] = None
model_path: Optional[str] = None # Path/reference to where the model can be loaded from
model_path: Optional[str] = (
None # Path/reference to where the model can be loaded from
)
model_id: Optional[str] = None # Unique identifier for the model instance

10 changes: 9 additions & 1 deletion src/deepiri_modelkit/contracts/events.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Event schemas for streaming service
"""

from pydantic import BaseModel, Field
from typing import Dict, Any, Optional
from datetime import datetime
Expand All @@ -9,6 +10,7 @@

class EventType(str, Enum):
"""Event type enumeration"""

MODEL_READY = "model-ready"
MODEL_LOADED = "model-loaded"
MODEL_FAILED = "model-failed"
Expand All @@ -26,6 +28,7 @@ class EventType(str, Enum):

class BaseEvent(BaseModel):
"""Base event schema"""

event: str
timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
source: str
Expand All @@ -34,6 +37,7 @@ class BaseEvent(BaseModel):

class ModelReadyEvent(BaseEvent):
"""Event published when model is trained and ready"""

event: str = EventType.MODEL_READY
model_name: str
version: str
Expand All @@ -46,6 +50,7 @@ class ModelReadyEvent(BaseEvent):

class ModelLoadedEvent(BaseEvent):
"""Event published when model is loaded in runtime"""

event: str = EventType.MODEL_LOADED
model_name: str
version: str
Expand All @@ -55,6 +60,7 @@ class ModelLoadedEvent(BaseEvent):

class InferenceEvent(BaseEvent):
"""Event published after inference completes"""

event: str = EventType.INFERENCE_COMPLETE
model_name: str
version: str
Expand All @@ -69,6 +75,7 @@ class InferenceEvent(BaseEvent):

class PlatformEvent(BaseEvent):
"""Event published by platform services"""

event: str # user-interaction, task-created, etc.
service: str
user_id: Optional[str] = None
Expand All @@ -79,6 +86,7 @@ class PlatformEvent(BaseEvent):

class AGIDecisionEvent(BaseEvent):
"""Event published by Cyrex-AGI for autonomous decisions"""

event: str = EventType.AGI_DECISION
decision_type: str
target_service: Optional[str] = None
Expand All @@ -89,11 +97,11 @@ class AGIDecisionEvent(BaseEvent):

class TrainingEvent(BaseEvent):
"""Event published during training"""

event: str # training-started, training-complete, training-failed
experiment_id: str
model_name: str
status: str
progress: Optional[float] = None # 0.0 to 1.0
metrics: Optional[Dict[str, Any]] = None
error: Optional[str] = None

56 changes: 31 additions & 25 deletions src/deepiri_modelkit/contracts/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""
Model contracts and interfaces
"""
from __future__ import annotations # Defer annotation evaluation to prevent Pydantic from processing Protocol types

from __future__ import (
annotations,
) # Defer annotation evaluation to prevent Pydantic from processing Protocol types

from typing import Protocol, Dict, Any, Optional, Annotated
from pydantic import BaseModel, Field, GetCoreSchemaHandler
Expand All @@ -11,13 +14,15 @@

class ModelInput(BaseModel):
"""Standard model input schema"""

data: Dict[str, Any]
metadata: Optional[Dict[str, Any]] = None
timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())


class ModelOutput(BaseModel):
"""Standard model output schema"""

prediction: Any
confidence: Optional[float] = None
metadata: Optional[Dict[str, Any]] = None
Expand All @@ -26,6 +31,7 @@ class ModelOutput(BaseModel):

class ModelMetadata(BaseModel):
"""Model metadata schema"""

name: str
version: str
description: Optional[str] = None
Expand All @@ -41,23 +47,23 @@ class AIModel(Protocol):
"""
Interface that all models must implement
Used by both Helox (training) and Cyrex (runtime)

Note: This is a Protocol (structural type). To use in Pydantic models,
use AIModelPydantic instead, which has full Pydantic schema support.
"""

def predict(self, input: ModelInput) -> ModelOutput:
"""Run inference on input"""
...

def get_metadata(self) -> ModelMetadata:
"""Get model metadata"""
...

def validate(self) -> bool:
"""Validate model is ready for use"""
...

def export(self, format: str = "onnx") -> str:
"""Export model to specified format, returns path"""
...
Expand All @@ -67,14 +73,14 @@ class AIModelPydantic:
"""
Pydantic-compatible wrapper for AIModel Protocol.
Implements __get_pydantic_core_schema__ to provide full schema support.

Usage in Pydantic models:
model: Optional[AIModelPydantic] = None

Note: In practice, model instances should be loaded separately and referenced
by ID/path rather than stored directly in serializable Pydantic models.
"""

@classmethod
def __get_pydantic_core_schema__(
cls,
Expand All @@ -83,61 +89,61 @@ def __get_pydantic_core_schema__(
) -> core_schema.CoreSchema:
"""
Pydantic Core Schema handler for AIModel Protocol.

This allows Pydantic to process AIModel types in model fields.
Since Protocols are structural types, we validate that the object
has the required methods rather than checking exact type.
"""

def validate_aimodel(value: Any) -> Any:
"""Validate that value implements AIModel Protocol interface"""
if value is None:
return None

# Check for required Protocol methods
required_methods = ['predict', 'get_metadata', 'validate', 'export']
required_methods = ["predict", "get_metadata", "validate", "export"]
missing_methods = [m for m in required_methods if not hasattr(value, m)]

if missing_methods:
raise ValueError(
f"Object does not implement AIModel Protocol. "
f"Missing methods: {', '.join(missing_methods)}"
)

return value

def serialize_aimodel(value: Any) -> Dict[str, Any]:
"""Serialize AIModel instance to dict"""
if value is None:
return None

# Try to get metadata if available
metadata = None
if hasattr(value, 'get_metadata'):
if hasattr(value, "get_metadata"):
try:
metadata = value.get_metadata()
# Convert ModelMetadata to dict if it's a Pydantic model
if hasattr(metadata, 'model_dump'):
if hasattr(metadata, "model_dump"):
metadata = metadata.model_dump()
elif hasattr(metadata, 'dict'):
elif hasattr(metadata, "dict"):
metadata = metadata.dict()
except Exception:
pass

return {
"type": "AIModel",
"metadata": metadata,
"has_predict": hasattr(value, 'predict'),
"has_validate": hasattr(value, 'validate'),
"has_predict": hasattr(value, "predict"),
"has_validate": hasattr(value, "validate"),
}

return core_schema.no_info_plain_validator_function(
validate_aimodel,
serialization=core_schema.plain_serializer_function_ser_schema(
serialize_aimodel
)
),
)


# ModelContract moved to contract.py to avoid Pydantic Protocol conflicts
# Import it from .contract import ModelContract when needed

36 changes: 11 additions & 25 deletions src/deepiri_modelkit/contracts/services.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,44 @@
"""
Service contracts and interfaces
"""

from typing import Protocol, Dict, Any, Optional
from pydantic import BaseModel


class ModelRegistryService(Protocol):
"""Interface for model registry operations"""

def register_model(
self,
model_name: str,
version: str,
model_path: str,
metadata: Dict[str, Any]
self, model_name: str, version: str, model_path: str, metadata: Dict[str, Any]
) -> bool:
"""Register a model in the registry"""
...

def get_model(
self,
model_name: str,
version: Optional[str] = None
self, model_name: str, version: Optional[str] = None
) -> Dict[str, Any]:
"""Get model information from registry"""
...

def list_models(self, model_name: Optional[str] = None) -> list:
"""List available models"""
...

def download_model(
self,
model_name: str,
version: str,
destination: str
) -> str:

def download_model(self, model_name: str, version: str, destination: str) -> str:
"""Download model to destination, returns local path"""
...


class StreamingService(Protocol):
"""Interface for streaming operations"""

def publish(self, topic: str, event: Dict[str, Any]) -> bool:
"""Publish event to topic"""
...

def subscribe(
self,
topic: str,
callback: callable,
consumer_group: Optional[str] = None
self, topic: str, callback: callable, consumer_group: Optional[str] = None
) -> None:
"""Subscribe to topic with callback"""
...

Loading
Loading