Coverage for agentos/enterprise/tenants.py: 42%
139 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
1"""
2AgentOS Enterprise — Multi-Tenant Management.
4功能:
5 - 租户创建/启停/删除
6 - 租户级配额管理(API 调用数、Token 数、并发数)
7 - 租户隔离(数据/配置/Agent 命名空间)
8 - 用量追踪与超限拦截
9 - 租户级自定义配置
10"""
12from __future__ import annotations
14import time
15from dataclasses import dataclass, field
16from enum import Enum
17from typing import Optional
20class TenantStatus(str, Enum):
21 ACTIVE = "active"
22 SUSPENDED = "suspended"
23 DELETED = "deleted"
26class TenantTier(str, Enum):
27 """租户等级。"""
28 FREE = "free" # 100 调用/天, 1 并发
29 STARTER = "starter" # 1000 调用/天, 3 并发
30 PRO = "pro" # 10000 调用/天, 10 并发
31 ENTERPRISE = "enterprise" # 自定义
34TIER_QUOTAS = {
35 TenantTier.FREE: {
36 "daily_api_calls": 100,
37 "daily_tokens": 100_000,
38 "max_concurrency": 1,
39 "max_agents": 3,
40 "max_api_keys": 2,
41 },
42 TenantTier.STARTER: {
43 "daily_api_calls": 1_000,
44 "daily_tokens": 1_000_000,
45 "max_concurrency": 3,
46 "max_agents": 10,
47 "max_api_keys": 5,
48 },
49 TenantTier.PRO: {
50 "daily_api_calls": 10_000,
51 "daily_tokens": 10_000_000,
52 "max_concurrency": 10,
53 "max_agents": 50,
54 "max_api_keys": 20,
55 },
56 TenantTier.ENTERPRISE: {
57 "daily_api_calls": 1_000_000,
58 "daily_tokens": 1_000_000_000,
59 "max_concurrency": 100,
60 "max_agents": 500,
61 "max_api_keys": 100,
62 },
63}
66@dataclass
67class TenantConfig:
68 """租户级配置覆盖。"""
69 default_model: str = "gpt-4o-mini"
70 default_provider: str = "openai"
71 allowed_providers: list[str] = field(default_factory=lambda: ["openai", "deepseek", "anthropic"])
72 max_iterations: int = 10
73 guardrail_level: str = "standard" # none / standard / strict
74 custom_settings: dict = field(default_factory=dict)
77@dataclass
78class TenantUsage:
79 """租户用量统计(当日)。"""
80 tenant_id: str
81 date: str # YYYY-MM-DD
82 api_calls: int = 0
83 tokens_used: int = 0
84 current_concurrency: int = 0
85 last_updated: float = field(default_factory=time.time)
88@dataclass
89class Tenant:
90 """租户实体。"""
91 tenant_id: str
92 name: str
93 tier: TenantTier
94 status: TenantStatus = TenantStatus.ACTIVE
95 config: TenantConfig = field(default_factory=TenantConfig)
96 created_at: float = field(default_factory=time.time)
97 updated_at: float = field(default_factory=time.time)
98 metadata: dict = field(default_factory=dict)
99 # 自定义配额覆盖(仅 Enterprise 级别可用)
100 custom_quotas: dict = field(default_factory=dict)
103class TenantManager:
104 """多租户管理器。
106 特性:
107 - 租户 CRUD + 启停
108 - 等级配额自动分配
109 - 用量追踪 + 超限拦截
110 - 租户级配置隔离
111 - 每日用量自动重置
112 """
114 def __init__(self):
115 self._tenants: dict[str, Tenant] = {}
116 self._usage: dict[str, TenantUsage] = {} # tenant_id → usage
118 # ── 租户管理 ──
120 def create_tenant(
121 self,
122 name: str,
123 tier: TenantTier = TenantTier.FREE,
124 config: Optional[TenantConfig] = None,
125 metadata: dict = None,
126 ) -> Tenant:
127 """创建租户。"""
128 import uuid
129 tenant_id = f"tn_{uuid.uuid4().hex[:12]}"
130 tenant = Tenant(
131 tenant_id=tenant_id,
132 name=name,
133 tier=tier,
134 config=config or TenantConfig(),
135 metadata=metadata or {},
136 )
137 self._tenants[tenant_id] = tenant
138 return tenant
140 def get_tenant(self, tenant_id: str) -> Optional[Tenant]:
141 return self._tenants.get(tenant_id)
143 def list_tenants(self, status: Optional[TenantStatus] = None) -> list[Tenant]:
144 tenants = list(self._tenants.values())
145 if status:
146 tenants = [t for t in tenants if t.status == status]
147 return sorted(tenants, key=lambda t: t.created_at)
149 def update_tenant(self, tenant_id: str, **kwargs) -> Optional[Tenant]:
150 tenant = self._tenants.get(tenant_id)
151 if not tenant:
152 return None
153 for k, v in kwargs.items():
154 if hasattr(tenant, k):
155 setattr(tenant, k, v)
156 tenant.updated_at = time.time()
157 return tenant
159 def suspend_tenant(self, tenant_id: str) -> bool:
160 t = self._tenants.get(tenant_id)
161 if not t:
162 return False
163 t.status = TenantStatus.SUSPENDED
164 t.updated_at = time.time()
165 return True
167 def activate_tenant(self, tenant_id: str) -> bool:
168 t = self._tenants.get(tenant_id)
169 if not t:
170 return False
171 t.status = TenantStatus.ACTIVE
172 t.updated_at = time.time()
173 return True
175 def delete_tenant(self, tenant_id: str) -> bool:
176 t = self._tenants.get(tenant_id)
177 if not t:
178 return False
179 t.status = TenantStatus.DELETED
180 t.updated_at = time.time()
181 return True
183 # ── 配额 ──
185 def get_quotas(self, tenant_id: str) -> dict:
186 """获取租户当前有效配额。"""
187 tenant = self._tenants.get(tenant_id)
188 if not tenant:
189 return {}
190 base = dict(TIER_QUOTAS.get(tenant.tier, {}))
191 base.update(tenant.custom_quotas)
192 return base
194 def check_quota(self, tenant_id: str, resource: str, amount: int = 1) -> tuple[bool, str]:
195 """检查配额是否允许此次操作。返回 (允许, 原因)。"""
196 tenant = self._tenants.get(tenant_id)
197 if not tenant:
198 return False, "租户不存在"
199 if tenant.status != TenantStatus.ACTIVE:
200 return False, f"租户状态: {tenant.status.value}"
202 quotas = self.get_quotas(tenant_id)
203 limit = quotas.get(resource)
205 if limit is None:
206 return True, ""
208 usage = self._get_usage(tenant_id)
209 current = getattr(usage, resource, 0)
211 if current + amount > limit:
212 return False, f"超出配额: {resource} ({current}/{limit})"
214 return True, ""
216 # ── 用量追踪 ──
218 def record_usage(self, tenant_id: str, api_calls: int = 0, tokens: int = 0, concurrency_delta: int = 0):
219 """记录一次用量。"""
220 if not self._tenants.get(tenant_id):
221 return
222 usage = self._get_usage(tenant_id)
223 usage.api_calls += api_calls
224 usage.tokens_used += tokens
225 usage.current_concurrency = max(0, usage.current_concurrency + concurrency_delta)
226 usage.last_updated = time.time()
228 def get_usage(self, tenant_id: str) -> Optional[TenantUsage]:
229 return self._get_usage(tenant_id)
231 def reset_daily_usage(self, tenant_id: str = None):
232 """重置每日用量(定时任务调用)。"""
233 if tenant_id:
234 self._usage.pop(tenant_id, None)
235 else:
236 self._usage.clear()
238 # ── 统计 ──
240 def stats(self) -> dict:
241 total = len(self._tenants)
242 by_tier = {}
243 by_status = {}
244 for t in self._tenants.values():
245 by_tier[t.tier.value] = by_tier.get(t.tier.value, 0) + 1
246 by_status[t.status.value] = by_status.get(t.status.value, 0) + 1
247 return {
248 "total": total,
249 "by_tier": by_tier,
250 "by_status": by_status,
251 }
253 # ── 内部 ──
255 def _get_usage(self, tenant_id: str) -> TenantUsage:
256 today = time.strftime("%Y-%m-%d")
257 key = f"{tenant_id}:{today}"
258 if key not in self._usage:
259 self._usage[key] = TenantUsage(tenant_id=tenant_id, date=today)
260 return self._usage[key]