Spaces:
Running
Running
File size: 12,190 Bytes
4d56236 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | """Contract models and schemas for inter-agent contract management."""
import hashlib
import json
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional
from uuid import UUID, uuid4
from pydantic import BaseModel, Field, validator
from sqlmodel import SQLModel, Field as SQLField, Relationship
class ContractState(str, Enum):
"""Contract lifecycle states."""
DRAFT = "draft"
PROPOSED = "proposed"
SIGNED = "signed"
REVOKED = "revoked"
EXPIRED = "expired"
class Clause(BaseModel):
"""A contract clause with structured content."""
id: str = Field(default_factory=lambda: str(uuid4()))
title: str = Field(..., description="Clause title")
content: str = Field(..., description="Clause content")
type: str = Field(default="general", description="Clause type (e.g., 'hipaa', 'data_handling')")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional clause metadata")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
class Signature(BaseModel):
"""Cryptographic signature for contract verification."""
signer_id: str = Field(..., description="ID of the signing party")
signer_type: str = Field(..., description="Type of signer (e.g., 'provider', 'payor', 'patient')")
signature: str = Field(..., description="Base64-encoded Ed25519 signature")
public_key: str = Field(..., description="Base64-encoded public key")
timestamp: datetime = Field(default_factory=datetime.utcnow)
metadata: Dict[str, Any] = Field(default_factory=dict)
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
def model_dump(self, **kwargs):
"""Override model_dump to handle datetime serialization."""
data = super().model_dump(**kwargs)
# Convert datetime objects to ISO format strings
for key, value in data.items():
if isinstance(value, datetime):
data[key] = value.isoformat()
return data
class Contract(SQLModel, table=True):
"""Contract model with SQLModel persistence."""
__tablename__ = "contracts"
# Primary fields
id: UUID = SQLField(default_factory=uuid4, primary_key=True)
title: str = SQLField(..., description="Contract title")
description: str = SQLField(..., description="Contract description")
# Contract content
clauses: str = SQLField(..., description="JSON-encoded clauses")
parties: str = SQLField(..., description="JSON-encoded parties")
# Lifecycle
state: ContractState = SQLField(default=ContractState.DRAFT)
created_at: datetime = SQLField(default_factory=datetime.utcnow)
proposed_at: Optional[datetime] = SQLField(default=None)
signed_at: Optional[datetime] = SQLField(default=None)
revoked_at: Optional[datetime] = SQLField(default=None)
expires_at: Optional[datetime] = SQLField(default=None)
# Signatures
signatures: str = SQLField(default="[]", description="JSON-encoded signatures")
# HIPAA compliance
is_hipaa_compliant: bool = SQLField(default=False)
hipaa_entities: str = SQLField(default="[]", description="JSON-encoded HIPAA entities")
# Metadata
contract_metadata: str = SQLField(default="{}", description="JSON-encoded metadata")
version: str = SQLField(default="1.0.0")
# Audit trail
created_by: str = SQLField(..., description="ID of the creating party")
last_modified: datetime = SQLField(default_factory=datetime.utcnow)
def get_clauses(self) -> List[Clause]:
"""Get parsed clauses from JSON."""
try:
clauses_data = json.loads(self.clauses)
return [Clause(**clause) for clause in clauses_data]
except (json.JSONDecodeError, ValueError):
return []
def set_clauses(self, clauses: List[Clause]) -> None:
"""Set clauses as JSON."""
self.clauses = json.dumps([clause.model_dump() for clause in clauses])
def get_parties(self) -> List[Dict[str, Any]]:
"""Get parsed parties from JSON."""
try:
return json.loads(self.parties)
except json.JSONDecodeError:
return []
def set_parties(self, parties: List[Dict[str, Any]]) -> None:
"""Set parties as JSON."""
self.parties = json.dumps(parties)
def get_signatures(self) -> List[Signature]:
"""Get parsed signatures from JSON."""
try:
signatures_data = json.loads(self.signatures)
return [Signature(**sig) for sig in signatures_data]
except (json.JSONDecodeError, ValueError):
return []
def set_signatures(self, signatures: List[Signature]) -> None:
"""Set signatures as JSON."""
self.signatures = json.dumps([sig.model_dump() for sig in signatures])
def get_hipaa_entities(self) -> List[Dict[str, Any]]:
"""Get parsed HIPAA entities from JSON."""
try:
return json.loads(self.hipaa_entities)
except json.JSONDecodeError:
return []
def set_hipaa_entities(self, entities: List[Dict[str, Any]]) -> None:
"""Set HIPAA entities as JSON."""
self.hipaa_entities = json.dumps(entities)
def get_metadata(self) -> Dict[str, Any]:
"""Get parsed metadata from JSON."""
try:
return json.loads(self.contract_metadata)
except json.JSONDecodeError:
return {}
def set_metadata(self, metadata: Dict[str, Any]) -> None:
"""Set metadata as JSON."""
self.contract_metadata = json.dumps(metadata)
def get_content_hash(self) -> str:
"""Get SHA-256 hash of contract content for signing."""
content = {
"id": str(self.id),
"title": self.title,
"description": self.description,
"clauses": self.clauses,
"parties": self.parties,
"version": self.version
}
content_str = json.dumps(content, sort_keys=True)
return hashlib.sha256(content_str.encode()).hexdigest()
def can_transition_to(self, new_state: ContractState) -> bool:
"""Check if contract can transition to new state."""
valid_transitions = {
ContractState.DRAFT: [ContractState.PROPOSED, ContractState.REVOKED],
ContractState.PROPOSED: [ContractState.SIGNED, ContractState.REVOKED, ContractState.DRAFT],
ContractState.SIGNED: [ContractState.REVOKED],
ContractState.REVOKED: [], # Terminal state
ContractState.EXPIRED: [] # Terminal state
}
return new_state in valid_transitions.get(self.state, [])
def is_fully_signed(self) -> bool:
"""Check if contract is fully signed by all required parties."""
parties = self.get_parties()
signatures = self.get_signatures()
# Check if all parties have signed
signed_party_ids = {sig.signer_id for sig in signatures}
required_party_ids = {party["id"] for party in parties}
return required_party_ids.issubset(signed_party_ids)
def get_unsigned_parties(self) -> List[Dict[str, Any]]:
"""Get parties that haven't signed yet."""
parties = self.get_parties()
signatures = self.get_signatures()
signed_party_ids = {sig.signer_id for sig in signatures}
return [party for party in parties if party["id"] not in signed_party_ids]
class ContractCreateRequest(BaseModel):
"""Request model for creating a contract."""
title: str = Field(..., description="Contract title")
description: str = Field(..., description="Contract description")
clauses: List[Clause] = Field(..., description="Contract clauses")
parties: List[Dict[str, Any]] = Field(..., description="Contract parties")
is_hipaa_compliant: bool = Field(default=False, description="HIPAA compliance flag")
hipaa_entities: Optional[List[Dict[str, Any]]] = Field(default=None, description="HIPAA entities")
expires_at: Optional[datetime] = Field(default=None, description="Contract expiration")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
version: str = Field(default="1.0.0", description="Contract version")
class ContractProposeRequest(BaseModel):
"""Request model for proposing a contract."""
proposed_to: List[str] = Field(..., description="IDs of parties to propose to")
message: Optional[str] = Field(default=None, description="Proposal message")
class ContractSignRequest(BaseModel):
"""Request model for signing a contract."""
signer_id: str = Field(..., description="ID of the signing party")
signer_type: str = Field(..., description="Type of signer")
public_key: str = Field(..., description="Base64-encoded public key")
signature: str = Field(..., description="Base64-encoded Ed25519 signature")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Signature metadata")
class ContractRevokeRequest(BaseModel):
"""Request model for revoking a contract."""
reason: str = Field(..., description="Reason for revocation")
revoked_by: str = Field(..., description="ID of the revoking party")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Revocation metadata")
class ContractResponse(BaseModel):
"""Response model for contract operations."""
id: str
title: str
description: str
clauses: List[Clause]
parties: List[Dict[str, Any]]
state: ContractState
created_at: datetime
proposed_at: Optional[datetime]
signed_at: Optional[datetime]
revoked_at: Optional[datetime]
expires_at: Optional[datetime]
signatures: List[Dict[str, Any]]
is_hipaa_compliant: bool
hipaa_entities: List[Dict[str, Any]]
contract_metadata: Dict[str, Any]
version: str
created_by: str
last_modified: datetime
content_hash: str
is_fully_signed: bool
unsigned_parties: List[Dict[str, Any]]
def model_dump(self, **kwargs):
"""Override model_dump to handle datetime serialization."""
data = super().model_dump(**kwargs)
# Convert datetime objects to ISO format strings
for key, value in data.items():
if isinstance(value, datetime):
data[key] = value.isoformat()
elif key == "signatures" and isinstance(value, list):
# Handle nested Signature objects
data[key] = [sig.model_dump() if hasattr(sig, 'model_dump') else sig for sig in value]
return data
@classmethod
def from_contract(cls, contract: Contract) -> "ContractResponse":
"""Create response from contract model."""
# Pre-serialize signatures to handle datetime fields
signatures = contract.get_signatures()
serialized_signatures = [sig.model_dump() for sig in signatures]
return cls(
id=str(contract.id), # Convert UUID to string
title=contract.title,
description=contract.description,
clauses=contract.get_clauses(),
parties=contract.get_parties(),
state=contract.state,
created_at=contract.created_at,
proposed_at=contract.proposed_at,
signed_at=contract.signed_at,
revoked_at=contract.revoked_at,
expires_at=contract.expires_at,
signatures=serialized_signatures, # Use pre-serialized signatures
is_hipaa_compliant=contract.is_hipaa_compliant,
hipaa_entities=contract.get_hipaa_entities(),
contract_metadata=contract.get_metadata(),
version=contract.version,
created_by=contract.created_by,
last_modified=contract.last_modified,
content_hash=contract.get_content_hash(),
is_fully_signed=contract.is_fully_signed(),
unsigned_parties=contract.get_unsigned_parties()
)
|