idun-agent-schema 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of idun-agent-schema might be problematic. Click here for more details.
- idun_agent_schema/__init__.py +10 -10
- idun_agent_schema/engine/__init__.py +11 -12
- idun_agent_schema/engine/agent.py +16 -16
- idun_agent_schema/engine/api.py +29 -29
- idun_agent_schema/engine/config.py +2 -7
- idun_agent_schema/engine/haystack.py +13 -13
- idun_agent_schema/engine/langgraph.py +47 -47
- idun_agent_schema/engine/server.py +15 -15
- idun_agent_schema/manager/__init__.py +50 -50
- idun_agent_schema/manager/api.py +158 -158
- idun_agent_schema/manager/deployments.py +12 -12
- idun_agent_schema/manager/deps.py +14 -14
- idun_agent_schema/manager/domain.py +276 -276
- idun_agent_schema/manager/dto.py +131 -131
- idun_agent_schema/manager/errors.py +22 -22
- idun_agent_schema/manager/settings.py +161 -161
- idun_agent_schema/shared/__init__.py +5 -5
- idun_agent_schema/shared/observability.py +56 -56
- {idun_agent_schema-0.1.4.dist-info → idun_agent_schema-0.1.6.dist-info}/METADATA +1 -1
- idun_agent_schema-0.1.6.dist-info/RECORD +22 -0
- idun_agent_schema-0.1.4.dist-info/RECORD +0 -22
- {idun_agent_schema-0.1.4.dist-info → idun_agent_schema-0.1.6.dist-info}/WHEEL +0 -0
|
@@ -1,276 +1,276 @@
|
|
|
1
|
-
"""Domain entities and enums for Agents and Tenants."""
|
|
2
|
-
|
|
3
|
-
from datetime import UTC, datetime
|
|
4
|
-
from enum import Enum
|
|
5
|
-
from typing import Any
|
|
6
|
-
from uuid import UUID, uuid4
|
|
7
|
-
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class AgentStatus(str, Enum):
|
|
12
|
-
"""Agent status enumeration."""
|
|
13
|
-
|
|
14
|
-
DRAFT = "draft"
|
|
15
|
-
ACTIVE = "active"
|
|
16
|
-
INACTIVE = "inactive"
|
|
17
|
-
DEPRECATED = "deprecated"
|
|
18
|
-
ERROR = "error"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class AgentFramework(str, Enum):
|
|
22
|
-
"""Supported agent frameworks."""
|
|
23
|
-
|
|
24
|
-
LANGGRAPH = "langgraph"
|
|
25
|
-
CREWAI = "crewai"
|
|
26
|
-
AUTOGEN = "autogen"
|
|
27
|
-
CUSTOM = "custom"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class AgentEntity(BaseModel):
|
|
31
|
-
"""Agent domain entity."""
|
|
32
|
-
|
|
33
|
-
id: UUID = Field(default_factory=uuid4)
|
|
34
|
-
name: str = Field(..., min_length=1, max_length=255)
|
|
35
|
-
description: str | None = Field(None, max_length=1000)
|
|
36
|
-
framework: AgentFramework
|
|
37
|
-
status: AgentStatus = Field(default=AgentStatus.DRAFT)
|
|
38
|
-
|
|
39
|
-
# Configuration
|
|
40
|
-
config: dict[str, Any] = Field(default_factory=dict)
|
|
41
|
-
environment_variables: dict[str, str] = Field(default_factory=dict)
|
|
42
|
-
|
|
43
|
-
# Metadata
|
|
44
|
-
version: str = Field(default="1.0.0")
|
|
45
|
-
tags: list[str] = Field(default_factory=list)
|
|
46
|
-
|
|
47
|
-
# Tenant isolation
|
|
48
|
-
tenant_id: UUID
|
|
49
|
-
|
|
50
|
-
# Timestamps
|
|
51
|
-
created_at: datetime
|
|
52
|
-
updated_at: datetime
|
|
53
|
-
deployed_at: datetime | None = None
|
|
54
|
-
|
|
55
|
-
# Performance metrics
|
|
56
|
-
total_runs: int = Field(default=0)
|
|
57
|
-
success_rate: float | None = Field(None, ge=0.0, le=1.0)
|
|
58
|
-
avg_response_time_ms: float | None = Field(None, ge=0)
|
|
59
|
-
|
|
60
|
-
def activate(self) -> None:
|
|
61
|
-
"""Transition agent from DRAFT to ACTIVE and set deployment time."""
|
|
62
|
-
if self.status == AgentStatus.DRAFT:
|
|
63
|
-
self.status = AgentStatus.ACTIVE
|
|
64
|
-
self.deployed_at = datetime.now(UTC)
|
|
65
|
-
else:
|
|
66
|
-
raise ValueError(f"Cannot activate agent in {self.status} status")
|
|
67
|
-
|
|
68
|
-
def deactivate(self) -> None:
|
|
69
|
-
"""Transition agent from ACTIVE to INACTIVE."""
|
|
70
|
-
if self.status == AgentStatus.ACTIVE:
|
|
71
|
-
self.status = AgentStatus.INACTIVE
|
|
72
|
-
else:
|
|
73
|
-
raise ValueError(f"Cannot deactivate agent in {self.status} status")
|
|
74
|
-
|
|
75
|
-
def update_metrics(self, success: bool, response_time_ms: float) -> None:
|
|
76
|
-
"""Update running success rate and average response time metrics."""
|
|
77
|
-
self.total_runs += 1
|
|
78
|
-
|
|
79
|
-
if self.success_rate is None:
|
|
80
|
-
self.success_rate = 1.0 if success else 0.0
|
|
81
|
-
else:
|
|
82
|
-
current_successes = self.success_rate * (self.total_runs - 1)
|
|
83
|
-
if success:
|
|
84
|
-
current_successes += 1
|
|
85
|
-
self.success_rate = current_successes / self.total_runs
|
|
86
|
-
|
|
87
|
-
if self.avg_response_time_ms is None:
|
|
88
|
-
self.avg_response_time_ms = response_time_ms
|
|
89
|
-
else:
|
|
90
|
-
total_time = self.avg_response_time_ms * (self.total_runs - 1)
|
|
91
|
-
self.avg_response_time_ms = (total_time + response_time_ms) / self.total_runs
|
|
92
|
-
|
|
93
|
-
def can_be_deployed(self) -> bool:
|
|
94
|
-
"""Return True if the agent is eligible for deployment."""
|
|
95
|
-
return (
|
|
96
|
-
self.status in [AgentStatus.DRAFT, AgentStatus.INACTIVE]
|
|
97
|
-
and bool(self.name)
|
|
98
|
-
and bool(self.config)
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class AgentRunEntity(BaseModel):
|
|
103
|
-
"""Agent run domain entity."""
|
|
104
|
-
|
|
105
|
-
id: UUID = Field(default_factory=uuid4)
|
|
106
|
-
agent_id: UUID
|
|
107
|
-
tenant_id: UUID
|
|
108
|
-
|
|
109
|
-
# Input/Output
|
|
110
|
-
input_data: dict[str, Any]
|
|
111
|
-
output_data: dict[str, Any] | None = None
|
|
112
|
-
|
|
113
|
-
# Execution details
|
|
114
|
-
status: str # running, completed, failed
|
|
115
|
-
started_at: datetime
|
|
116
|
-
completed_at: datetime | None = None
|
|
117
|
-
error_message: str | None = None
|
|
118
|
-
|
|
119
|
-
# Performance
|
|
120
|
-
response_time_ms: float | None = None
|
|
121
|
-
tokens_used: int | None = None
|
|
122
|
-
cost_usd: float | None = None
|
|
123
|
-
|
|
124
|
-
# Tracing
|
|
125
|
-
trace_id: str | None = None
|
|
126
|
-
span_id: str | None = None
|
|
127
|
-
|
|
128
|
-
def complete(self, output_data: dict[str, Any], response_time_ms: float) -> None:
|
|
129
|
-
"""Mark run as completed with outputs and timing."""
|
|
130
|
-
self.status = "completed"
|
|
131
|
-
self.output_data = output_data
|
|
132
|
-
self.completed_at = datetime.now(UTC)
|
|
133
|
-
self.response_time_ms = response_time_ms
|
|
134
|
-
|
|
135
|
-
def fail(self, error_message: str) -> None:
|
|
136
|
-
"""Mark run as failed with an error message."""
|
|
137
|
-
self.status = "failed"
|
|
138
|
-
self.error_message = error_message
|
|
139
|
-
self.completed_at = datetime.now(UTC)
|
|
140
|
-
|
|
141
|
-
@property
|
|
142
|
-
def is_completed(self) -> bool:
|
|
143
|
-
"""Return True if this run has finished (completed or failed)."""
|
|
144
|
-
return self.status in ["completed", "failed"]
|
|
145
|
-
|
|
146
|
-
@property
|
|
147
|
-
def was_successful(self) -> bool:
|
|
148
|
-
"""Return True if the run completed successfully."""
|
|
149
|
-
return self.status == "completed"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
class TenantStatus(str, Enum):
|
|
153
|
-
"""Tenant status enumeration."""
|
|
154
|
-
|
|
155
|
-
ACTIVE = "active"
|
|
156
|
-
SUSPENDED = "suspended"
|
|
157
|
-
DEACTIVATED = "deactivated"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
class TenantPlan(str, Enum):
|
|
161
|
-
"""Tenant subscription plan."""
|
|
162
|
-
|
|
163
|
-
FREE = "free"
|
|
164
|
-
STARTER = "starter"
|
|
165
|
-
PROFESSIONAL = "professional"
|
|
166
|
-
ENTERPRISE = "enterprise"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class TenantEntity(BaseModel):
|
|
170
|
-
"""Tenant domain entity."""
|
|
171
|
-
|
|
172
|
-
id: UUID = Field(default_factory=uuid4)
|
|
173
|
-
name: str = Field(..., min_length=1, max_length=255)
|
|
174
|
-
slug: str = Field(..., min_length=1, max_length=100)
|
|
175
|
-
|
|
176
|
-
# Contact information
|
|
177
|
-
email: str = Field(..., description="Primary contact email")
|
|
178
|
-
website: str | None = Field(None)
|
|
179
|
-
|
|
180
|
-
# Status and plan
|
|
181
|
-
status: TenantStatus = Field(default=TenantStatus.ACTIVE)
|
|
182
|
-
plan: TenantPlan = Field(default=TenantPlan.FREE)
|
|
183
|
-
|
|
184
|
-
# Settings
|
|
185
|
-
settings: dict[str, Any] = Field(default_factory=dict)
|
|
186
|
-
|
|
187
|
-
# Quotas and limits
|
|
188
|
-
max_agents: int = Field(default=5)
|
|
189
|
-
max_runs_per_month: int = Field(default=1000)
|
|
190
|
-
max_storage_mb: int = Field(default=100)
|
|
191
|
-
|
|
192
|
-
# Usage tracking
|
|
193
|
-
current_agents: int = Field(default=0)
|
|
194
|
-
current_runs_this_month: int = Field(default=0)
|
|
195
|
-
current_storage_mb: float = Field(default=0.0)
|
|
196
|
-
|
|
197
|
-
# Timestamps
|
|
198
|
-
created_at: datetime
|
|
199
|
-
updated_at: datetime
|
|
200
|
-
suspended_at: datetime | None = None
|
|
201
|
-
|
|
202
|
-
def can_create_agent(self) -> bool:
|
|
203
|
-
"""Return True if tenant has capacity to create a new agent."""
|
|
204
|
-
return self.status == TenantStatus.ACTIVE and self.current_agents < self.max_agents
|
|
205
|
-
|
|
206
|
-
def can_run_agent(self) -> bool:
|
|
207
|
-
"""Return True if tenant is below monthly run quota."""
|
|
208
|
-
return self.status == TenantStatus.ACTIVE and self.current_runs_this_month < self.max_runs_per_month
|
|
209
|
-
|
|
210
|
-
def suspend(self, reason: str) -> None:
|
|
211
|
-
"""Suspend tenant and record the reason."""
|
|
212
|
-
self.status = TenantStatus.SUSPENDED
|
|
213
|
-
self.suspended_at = datetime.utcnow()
|
|
214
|
-
self.settings["suspension_reason"] = reason
|
|
215
|
-
|
|
216
|
-
def reactivate(self) -> None:
|
|
217
|
-
"""Reactivate a previously suspended tenant."""
|
|
218
|
-
if self.status == TenantStatus.SUSPENDED:
|
|
219
|
-
self.status = TenantStatus.ACTIVE
|
|
220
|
-
self.suspended_at = None
|
|
221
|
-
if "suspension_reason" in self.settings:
|
|
222
|
-
del self.settings["suspension_reason"]
|
|
223
|
-
|
|
224
|
-
def upgrade_plan(self, new_plan: TenantPlan) -> None:
|
|
225
|
-
"""Upgrade plan and adjust quotas accordingly."""
|
|
226
|
-
self.plan = new_plan
|
|
227
|
-
if new_plan == TenantPlan.STARTER:
|
|
228
|
-
self.max_agents = 20
|
|
229
|
-
self.max_runs_per_month = 10000
|
|
230
|
-
self.max_storage_mb = 1000
|
|
231
|
-
elif new_plan == TenantPlan.PROFESSIONAL:
|
|
232
|
-
self.max_agents = 100
|
|
233
|
-
self.max_runs_per_month = 100000
|
|
234
|
-
self.max_storage_mb = 10000
|
|
235
|
-
elif new_plan == TenantPlan.ENTERPRISE:
|
|
236
|
-
self.max_agents = 1000
|
|
237
|
-
self.max_runs_per_month = 1000000
|
|
238
|
-
self.max_storage_mb = 100000
|
|
239
|
-
|
|
240
|
-
def increment_usage(self, agents: int = 0, runs: int = 0, storage_mb: float = 0) -> None:
|
|
241
|
-
"""Increment tracked usage counters."""
|
|
242
|
-
self.current_agents += agents
|
|
243
|
-
self.current_runs_this_month += runs
|
|
244
|
-
self.current_storage_mb += storage_mb
|
|
245
|
-
|
|
246
|
-
def reset_monthly_usage(self) -> None:
|
|
247
|
-
"""Reset monthly run counter at billing cycle start."""
|
|
248
|
-
self.current_runs_this_month = 0
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
class TenantUserEntity(BaseModel):
|
|
252
|
-
"""Tenant user domain entity for multi-user tenants."""
|
|
253
|
-
|
|
254
|
-
id: UUID = Field(default_factory=uuid4)
|
|
255
|
-
tenant_id: UUID
|
|
256
|
-
user_id: str
|
|
257
|
-
email: str
|
|
258
|
-
|
|
259
|
-
# Role and permissions
|
|
260
|
-
role: str = Field(default="member")
|
|
261
|
-
permissions: list[str] = Field(default_factory=list)
|
|
262
|
-
|
|
263
|
-
# Status
|
|
264
|
-
is_active: bool = Field(default=True)
|
|
265
|
-
|
|
266
|
-
# Timestamps
|
|
267
|
-
joined_at: datetime
|
|
268
|
-
last_active_at: datetime | None = None
|
|
269
|
-
|
|
270
|
-
def has_permission(self, permission: str) -> bool:
|
|
271
|
-
"""Return True if the user has an explicit permission or is owner."""
|
|
272
|
-
return permission in self.permissions or self.role == "owner"
|
|
273
|
-
|
|
274
|
-
def is_admin(self) -> bool:
|
|
275
|
-
"""Return True if the user is an admin or owner."""
|
|
276
|
-
return self.role in ["owner", "admin"]
|
|
1
|
+
"""Domain entities and enums for Agents and Tenants."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import UUID, uuid4
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AgentStatus(str, Enum):
|
|
12
|
+
"""Agent status enumeration."""
|
|
13
|
+
|
|
14
|
+
DRAFT = "draft"
|
|
15
|
+
ACTIVE = "active"
|
|
16
|
+
INACTIVE = "inactive"
|
|
17
|
+
DEPRECATED = "deprecated"
|
|
18
|
+
ERROR = "error"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentFramework(str, Enum):
|
|
22
|
+
"""Supported agent frameworks."""
|
|
23
|
+
|
|
24
|
+
LANGGRAPH = "langgraph"
|
|
25
|
+
CREWAI = "crewai"
|
|
26
|
+
AUTOGEN = "autogen"
|
|
27
|
+
CUSTOM = "custom"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AgentEntity(BaseModel):
|
|
31
|
+
"""Agent domain entity."""
|
|
32
|
+
|
|
33
|
+
id: UUID = Field(default_factory=uuid4)
|
|
34
|
+
name: str = Field(..., min_length=1, max_length=255)
|
|
35
|
+
description: str | None = Field(None, max_length=1000)
|
|
36
|
+
framework: AgentFramework
|
|
37
|
+
status: AgentStatus = Field(default=AgentStatus.DRAFT)
|
|
38
|
+
|
|
39
|
+
# Configuration
|
|
40
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
41
|
+
environment_variables: dict[str, str] = Field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
# Metadata
|
|
44
|
+
version: str = Field(default="1.0.0")
|
|
45
|
+
tags: list[str] = Field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
# Tenant isolation
|
|
48
|
+
tenant_id: UUID
|
|
49
|
+
|
|
50
|
+
# Timestamps
|
|
51
|
+
created_at: datetime
|
|
52
|
+
updated_at: datetime
|
|
53
|
+
deployed_at: datetime | None = None
|
|
54
|
+
|
|
55
|
+
# Performance metrics
|
|
56
|
+
total_runs: int = Field(default=0)
|
|
57
|
+
success_rate: float | None = Field(None, ge=0.0, le=1.0)
|
|
58
|
+
avg_response_time_ms: float | None = Field(None, ge=0)
|
|
59
|
+
|
|
60
|
+
def activate(self) -> None:
|
|
61
|
+
"""Transition agent from DRAFT to ACTIVE and set deployment time."""
|
|
62
|
+
if self.status == AgentStatus.DRAFT:
|
|
63
|
+
self.status = AgentStatus.ACTIVE
|
|
64
|
+
self.deployed_at = datetime.now(UTC)
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError(f"Cannot activate agent in {self.status} status")
|
|
67
|
+
|
|
68
|
+
def deactivate(self) -> None:
|
|
69
|
+
"""Transition agent from ACTIVE to INACTIVE."""
|
|
70
|
+
if self.status == AgentStatus.ACTIVE:
|
|
71
|
+
self.status = AgentStatus.INACTIVE
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(f"Cannot deactivate agent in {self.status} status")
|
|
74
|
+
|
|
75
|
+
def update_metrics(self, success: bool, response_time_ms: float) -> None:
|
|
76
|
+
"""Update running success rate and average response time metrics."""
|
|
77
|
+
self.total_runs += 1
|
|
78
|
+
|
|
79
|
+
if self.success_rate is None:
|
|
80
|
+
self.success_rate = 1.0 if success else 0.0
|
|
81
|
+
else:
|
|
82
|
+
current_successes = self.success_rate * (self.total_runs - 1)
|
|
83
|
+
if success:
|
|
84
|
+
current_successes += 1
|
|
85
|
+
self.success_rate = current_successes / self.total_runs
|
|
86
|
+
|
|
87
|
+
if self.avg_response_time_ms is None:
|
|
88
|
+
self.avg_response_time_ms = response_time_ms
|
|
89
|
+
else:
|
|
90
|
+
total_time = self.avg_response_time_ms * (self.total_runs - 1)
|
|
91
|
+
self.avg_response_time_ms = (total_time + response_time_ms) / self.total_runs
|
|
92
|
+
|
|
93
|
+
def can_be_deployed(self) -> bool:
|
|
94
|
+
"""Return True if the agent is eligible for deployment."""
|
|
95
|
+
return (
|
|
96
|
+
self.status in [AgentStatus.DRAFT, AgentStatus.INACTIVE]
|
|
97
|
+
and bool(self.name)
|
|
98
|
+
and bool(self.config)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AgentRunEntity(BaseModel):
|
|
103
|
+
"""Agent run domain entity."""
|
|
104
|
+
|
|
105
|
+
id: UUID = Field(default_factory=uuid4)
|
|
106
|
+
agent_id: UUID
|
|
107
|
+
tenant_id: UUID
|
|
108
|
+
|
|
109
|
+
# Input/Output
|
|
110
|
+
input_data: dict[str, Any]
|
|
111
|
+
output_data: dict[str, Any] | None = None
|
|
112
|
+
|
|
113
|
+
# Execution details
|
|
114
|
+
status: str # running, completed, failed
|
|
115
|
+
started_at: datetime
|
|
116
|
+
completed_at: datetime | None = None
|
|
117
|
+
error_message: str | None = None
|
|
118
|
+
|
|
119
|
+
# Performance
|
|
120
|
+
response_time_ms: float | None = None
|
|
121
|
+
tokens_used: int | None = None
|
|
122
|
+
cost_usd: float | None = None
|
|
123
|
+
|
|
124
|
+
# Tracing
|
|
125
|
+
trace_id: str | None = None
|
|
126
|
+
span_id: str | None = None
|
|
127
|
+
|
|
128
|
+
def complete(self, output_data: dict[str, Any], response_time_ms: float) -> None:
|
|
129
|
+
"""Mark run as completed with outputs and timing."""
|
|
130
|
+
self.status = "completed"
|
|
131
|
+
self.output_data = output_data
|
|
132
|
+
self.completed_at = datetime.now(UTC)
|
|
133
|
+
self.response_time_ms = response_time_ms
|
|
134
|
+
|
|
135
|
+
def fail(self, error_message: str) -> None:
|
|
136
|
+
"""Mark run as failed with an error message."""
|
|
137
|
+
self.status = "failed"
|
|
138
|
+
self.error_message = error_message
|
|
139
|
+
self.completed_at = datetime.now(UTC)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def is_completed(self) -> bool:
|
|
143
|
+
"""Return True if this run has finished (completed or failed)."""
|
|
144
|
+
return self.status in ["completed", "failed"]
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def was_successful(self) -> bool:
|
|
148
|
+
"""Return True if the run completed successfully."""
|
|
149
|
+
return self.status == "completed"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TenantStatus(str, Enum):
|
|
153
|
+
"""Tenant status enumeration."""
|
|
154
|
+
|
|
155
|
+
ACTIVE = "active"
|
|
156
|
+
SUSPENDED = "suspended"
|
|
157
|
+
DEACTIVATED = "deactivated"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TenantPlan(str, Enum):
|
|
161
|
+
"""Tenant subscription plan."""
|
|
162
|
+
|
|
163
|
+
FREE = "free"
|
|
164
|
+
STARTER = "starter"
|
|
165
|
+
PROFESSIONAL = "professional"
|
|
166
|
+
ENTERPRISE = "enterprise"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TenantEntity(BaseModel):
|
|
170
|
+
"""Tenant domain entity."""
|
|
171
|
+
|
|
172
|
+
id: UUID = Field(default_factory=uuid4)
|
|
173
|
+
name: str = Field(..., min_length=1, max_length=255)
|
|
174
|
+
slug: str = Field(..., min_length=1, max_length=100)
|
|
175
|
+
|
|
176
|
+
# Contact information
|
|
177
|
+
email: str = Field(..., description="Primary contact email")
|
|
178
|
+
website: str | None = Field(None)
|
|
179
|
+
|
|
180
|
+
# Status and plan
|
|
181
|
+
status: TenantStatus = Field(default=TenantStatus.ACTIVE)
|
|
182
|
+
plan: TenantPlan = Field(default=TenantPlan.FREE)
|
|
183
|
+
|
|
184
|
+
# Settings
|
|
185
|
+
settings: dict[str, Any] = Field(default_factory=dict)
|
|
186
|
+
|
|
187
|
+
# Quotas and limits
|
|
188
|
+
max_agents: int = Field(default=5)
|
|
189
|
+
max_runs_per_month: int = Field(default=1000)
|
|
190
|
+
max_storage_mb: int = Field(default=100)
|
|
191
|
+
|
|
192
|
+
# Usage tracking
|
|
193
|
+
current_agents: int = Field(default=0)
|
|
194
|
+
current_runs_this_month: int = Field(default=0)
|
|
195
|
+
current_storage_mb: float = Field(default=0.0)
|
|
196
|
+
|
|
197
|
+
# Timestamps
|
|
198
|
+
created_at: datetime
|
|
199
|
+
updated_at: datetime
|
|
200
|
+
suspended_at: datetime | None = None
|
|
201
|
+
|
|
202
|
+
def can_create_agent(self) -> bool:
|
|
203
|
+
"""Return True if tenant has capacity to create a new agent."""
|
|
204
|
+
return self.status == TenantStatus.ACTIVE and self.current_agents < self.max_agents
|
|
205
|
+
|
|
206
|
+
def can_run_agent(self) -> bool:
|
|
207
|
+
"""Return True if tenant is below monthly run quota."""
|
|
208
|
+
return self.status == TenantStatus.ACTIVE and self.current_runs_this_month < self.max_runs_per_month
|
|
209
|
+
|
|
210
|
+
def suspend(self, reason: str) -> None:
|
|
211
|
+
"""Suspend tenant and record the reason."""
|
|
212
|
+
self.status = TenantStatus.SUSPENDED
|
|
213
|
+
self.suspended_at = datetime.utcnow()
|
|
214
|
+
self.settings["suspension_reason"] = reason
|
|
215
|
+
|
|
216
|
+
def reactivate(self) -> None:
|
|
217
|
+
"""Reactivate a previously suspended tenant."""
|
|
218
|
+
if self.status == TenantStatus.SUSPENDED:
|
|
219
|
+
self.status = TenantStatus.ACTIVE
|
|
220
|
+
self.suspended_at = None
|
|
221
|
+
if "suspension_reason" in self.settings:
|
|
222
|
+
del self.settings["suspension_reason"]
|
|
223
|
+
|
|
224
|
+
def upgrade_plan(self, new_plan: TenantPlan) -> None:
|
|
225
|
+
"""Upgrade plan and adjust quotas accordingly."""
|
|
226
|
+
self.plan = new_plan
|
|
227
|
+
if new_plan == TenantPlan.STARTER:
|
|
228
|
+
self.max_agents = 20
|
|
229
|
+
self.max_runs_per_month = 10000
|
|
230
|
+
self.max_storage_mb = 1000
|
|
231
|
+
elif new_plan == TenantPlan.PROFESSIONAL:
|
|
232
|
+
self.max_agents = 100
|
|
233
|
+
self.max_runs_per_month = 100000
|
|
234
|
+
self.max_storage_mb = 10000
|
|
235
|
+
elif new_plan == TenantPlan.ENTERPRISE:
|
|
236
|
+
self.max_agents = 1000
|
|
237
|
+
self.max_runs_per_month = 1000000
|
|
238
|
+
self.max_storage_mb = 100000
|
|
239
|
+
|
|
240
|
+
def increment_usage(self, agents: int = 0, runs: int = 0, storage_mb: float = 0) -> None:
|
|
241
|
+
"""Increment tracked usage counters."""
|
|
242
|
+
self.current_agents += agents
|
|
243
|
+
self.current_runs_this_month += runs
|
|
244
|
+
self.current_storage_mb += storage_mb
|
|
245
|
+
|
|
246
|
+
def reset_monthly_usage(self) -> None:
|
|
247
|
+
"""Reset monthly run counter at billing cycle start."""
|
|
248
|
+
self.current_runs_this_month = 0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TenantUserEntity(BaseModel):
|
|
252
|
+
"""Tenant user domain entity for multi-user tenants."""
|
|
253
|
+
|
|
254
|
+
id: UUID = Field(default_factory=uuid4)
|
|
255
|
+
tenant_id: UUID
|
|
256
|
+
user_id: str
|
|
257
|
+
email: str
|
|
258
|
+
|
|
259
|
+
# Role and permissions
|
|
260
|
+
role: str = Field(default="member")
|
|
261
|
+
permissions: list[str] = Field(default_factory=list)
|
|
262
|
+
|
|
263
|
+
# Status
|
|
264
|
+
is_active: bool = Field(default=True)
|
|
265
|
+
|
|
266
|
+
# Timestamps
|
|
267
|
+
joined_at: datetime
|
|
268
|
+
last_active_at: datetime | None = None
|
|
269
|
+
|
|
270
|
+
def has_permission(self, permission: str) -> bool:
|
|
271
|
+
"""Return True if the user has an explicit permission or is owner."""
|
|
272
|
+
return permission in self.permissions or self.role == "owner"
|
|
273
|
+
|
|
274
|
+
def is_admin(self) -> bool:
|
|
275
|
+
"""Return True if the user is an admin or owner."""
|
|
276
|
+
return self.role in ["owner", "admin"]
|