foundry-mcp 0.8.22__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 foundry-mcp might be problematic. Click here for more details.
- foundry_mcp/__init__.py +13 -0
- foundry_mcp/cli/__init__.py +67 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +640 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +667 -0
- foundry_mcp/cli/commands/session.py +472 -0
- foundry_mcp/cli/commands/specs.py +686 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +298 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +1454 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1773 -0
- foundry_mcp/core/batch_operations.py +1202 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/journal.py +700 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1376 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +146 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +387 -0
- foundry_mcp/core/prometheus.py +564 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +691 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
- foundry_mcp/core/prompts/plan_review.py +627 -0
- foundry_mcp/core/providers/__init__.py +237 -0
- foundry_mcp/core/providers/base.py +515 -0
- foundry_mcp/core/providers/claude.py +472 -0
- foundry_mcp/core/providers/codex.py +637 -0
- foundry_mcp/core/providers/cursor_agent.py +630 -0
- foundry_mcp/core/providers/detectors.py +515 -0
- foundry_mcp/core/providers/gemini.py +426 -0
- foundry_mcp/core/providers/opencode.py +718 -0
- foundry_mcp/core/providers/opencode_wrapper.js +308 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +857 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1234 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4142 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +1624 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +4119 -0
- foundry_mcp/core/task.py +2463 -0
- foundry_mcp/core/testing.py +839 -0
- foundry_mcp/core/validation.py +2357 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +177 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +300 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +164 -0
- foundry_mcp/dashboard/views/overview.py +96 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +414 -0
- foundry_mcp/server.py +150 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +92 -0
- foundry_mcp/tools/unified/authoring.py +3620 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +268 -0
- foundry_mcp/tools/unified/environment.py +1341 -0
- foundry_mcp/tools/unified/error.py +479 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +640 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +876 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +589 -0
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +1042 -0
- foundry_mcp/tools/unified/review_helpers.py +314 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +565 -0
- foundry_mcp/tools/unified/spec.py +1283 -0
- foundry_mcp/tools/unified/task.py +3846 -0
- foundry_mcp/tools/unified/test.py +431 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.8.22.dist-info/METADATA +344 -0
- foundry_mcp-0.8.22.dist-info/RECORD +153 -0
- foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
- foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rate limiting module for foundry-mcp.
|
|
3
|
+
|
|
4
|
+
Provides per-tool rate limiting with configurable limits and audit logging.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
15
|
+
|
|
16
|
+
from foundry_mcp.core.observability import audit_log
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Schema version
|
|
22
|
+
SCHEMA_VERSION = "1.0.0"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class RateLimitConfig:
|
|
27
|
+
"""
|
|
28
|
+
Configuration for a rate limit.
|
|
29
|
+
"""
|
|
30
|
+
requests_per_minute: int = 60
|
|
31
|
+
burst_limit: int = 10
|
|
32
|
+
enabled: bool = True
|
|
33
|
+
reason: str = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class RateLimitState:
|
|
38
|
+
"""
|
|
39
|
+
Current state of a rate limiter.
|
|
40
|
+
"""
|
|
41
|
+
tokens: float = 0.0
|
|
42
|
+
last_update: float = 0.0
|
|
43
|
+
request_count: int = 0
|
|
44
|
+
throttle_count: int = 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class RateLimitResult:
|
|
49
|
+
"""
|
|
50
|
+
Result of a rate limit check.
|
|
51
|
+
"""
|
|
52
|
+
allowed: bool
|
|
53
|
+
remaining: int = 0
|
|
54
|
+
reset_in: float = 0.0
|
|
55
|
+
limit: int = 0
|
|
56
|
+
reason: str = ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TokenBucketLimiter:
|
|
60
|
+
"""
|
|
61
|
+
Token bucket rate limiter implementation.
|
|
62
|
+
|
|
63
|
+
Provides smooth rate limiting with burst support.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, config: RateLimitConfig):
|
|
67
|
+
"""
|
|
68
|
+
Initialize rate limiter.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
config: Rate limit configuration
|
|
72
|
+
"""
|
|
73
|
+
self.config = config
|
|
74
|
+
self.state = RateLimitState(
|
|
75
|
+
tokens=float(config.burst_limit),
|
|
76
|
+
last_update=time.time()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def check(self) -> RateLimitResult:
|
|
80
|
+
"""
|
|
81
|
+
Check if a request is allowed without consuming tokens.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
RateLimitResult indicating if request would be allowed
|
|
85
|
+
"""
|
|
86
|
+
if not self.config.enabled:
|
|
87
|
+
return RateLimitResult(allowed=True, remaining=-1)
|
|
88
|
+
|
|
89
|
+
self._refill()
|
|
90
|
+
return RateLimitResult(
|
|
91
|
+
allowed=self.state.tokens >= 1.0,
|
|
92
|
+
remaining=int(self.state.tokens),
|
|
93
|
+
reset_in=self._time_to_next_token(),
|
|
94
|
+
limit=self.config.requests_per_minute,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def acquire(self) -> RateLimitResult:
|
|
98
|
+
"""
|
|
99
|
+
Attempt to acquire a token for a request.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
RateLimitResult indicating if request is allowed
|
|
103
|
+
"""
|
|
104
|
+
if not self.config.enabled:
|
|
105
|
+
return RateLimitResult(allowed=True, remaining=-1)
|
|
106
|
+
|
|
107
|
+
self._refill()
|
|
108
|
+
self.state.request_count += 1
|
|
109
|
+
|
|
110
|
+
if self.state.tokens >= 1.0:
|
|
111
|
+
self.state.tokens -= 1.0
|
|
112
|
+
return RateLimitResult(
|
|
113
|
+
allowed=True,
|
|
114
|
+
remaining=int(self.state.tokens),
|
|
115
|
+
reset_in=self._time_to_next_token(),
|
|
116
|
+
limit=self.config.requests_per_minute,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
self.state.throttle_count += 1
|
|
120
|
+
return RateLimitResult(
|
|
121
|
+
allowed=False,
|
|
122
|
+
remaining=0,
|
|
123
|
+
reset_in=self._time_to_next_token(),
|
|
124
|
+
limit=self.config.requests_per_minute,
|
|
125
|
+
reason=self.config.reason or "Rate limit exceeded",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _refill(self) -> None:
|
|
129
|
+
"""Refill tokens based on elapsed time."""
|
|
130
|
+
now = time.time()
|
|
131
|
+
elapsed = now - self.state.last_update
|
|
132
|
+
self.state.last_update = now
|
|
133
|
+
|
|
134
|
+
# Calculate tokens to add (tokens per second = rpm / 60)
|
|
135
|
+
tokens_per_second = self.config.requests_per_minute / 60.0
|
|
136
|
+
new_tokens = elapsed * tokens_per_second
|
|
137
|
+
|
|
138
|
+
self.state.tokens = min(
|
|
139
|
+
float(self.config.burst_limit),
|
|
140
|
+
self.state.tokens + new_tokens
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def _time_to_next_token(self) -> float:
|
|
144
|
+
"""Calculate time until next token is available."""
|
|
145
|
+
if self.state.tokens >= 1.0:
|
|
146
|
+
return 0.0
|
|
147
|
+
tokens_per_second = self.config.requests_per_minute / 60.0
|
|
148
|
+
needed = 1.0 - self.state.tokens
|
|
149
|
+
return needed / tokens_per_second
|
|
150
|
+
|
|
151
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
152
|
+
"""Get limiter statistics."""
|
|
153
|
+
return {
|
|
154
|
+
"requests": self.state.request_count,
|
|
155
|
+
"throttled": self.state.throttle_count,
|
|
156
|
+
"current_tokens": int(self.state.tokens),
|
|
157
|
+
"limit": self.config.requests_per_minute,
|
|
158
|
+
"burst": self.config.burst_limit,
|
|
159
|
+
"enabled": self.config.enabled,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class RateLimitManager:
|
|
164
|
+
"""
|
|
165
|
+
Manages rate limits for multiple tools/operations.
|
|
166
|
+
|
|
167
|
+
Loads configuration from manifest and environment,
|
|
168
|
+
enforces limits, and logs throttling events.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(self):
|
|
172
|
+
"""Initialize rate limit manager."""
|
|
173
|
+
self._limiters: Dict[str, TokenBucketLimiter] = {}
|
|
174
|
+
self._tenant_limiters: Dict[str, Dict[str, TokenBucketLimiter]] = defaultdict(dict)
|
|
175
|
+
self._global_config = RateLimitConfig()
|
|
176
|
+
self._tool_configs: Dict[str, RateLimitConfig] = {}
|
|
177
|
+
|
|
178
|
+
def load_from_manifest(self, manifest_path: Optional[Path] = None) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Load rate limit configurations from capabilities manifest.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
manifest_path: Path to manifest file (auto-detected if not provided)
|
|
184
|
+
"""
|
|
185
|
+
if manifest_path is None:
|
|
186
|
+
search_paths = [
|
|
187
|
+
Path.cwd() / "mcp" / "capabilities_manifest.json",
|
|
188
|
+
Path(__file__).parent.parent.parent / "mcp" / "capabilities_manifest.json",
|
|
189
|
+
]
|
|
190
|
+
for path in search_paths:
|
|
191
|
+
if path.exists():
|
|
192
|
+
manifest_path = path
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
if not manifest_path or not manifest_path.exists():
|
|
196
|
+
logger.debug("No manifest found for rate limit configuration")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
with open(manifest_path, "r") as f:
|
|
201
|
+
manifest = json.load(f)
|
|
202
|
+
|
|
203
|
+
# Load tool-specific rate limits
|
|
204
|
+
for category, tools in manifest.get("tools", {}).items():
|
|
205
|
+
if isinstance(tools, list):
|
|
206
|
+
for tool in tools:
|
|
207
|
+
name = tool.get("name")
|
|
208
|
+
rate_limit = tool.get("rate_limit")
|
|
209
|
+
if name and rate_limit:
|
|
210
|
+
self._tool_configs[name] = RateLimitConfig(
|
|
211
|
+
requests_per_minute=rate_limit.get("requests_per_minute", 60),
|
|
212
|
+
burst_limit=rate_limit.get("burst_limit", 10),
|
|
213
|
+
enabled=True,
|
|
214
|
+
reason=rate_limit.get("reason", ""),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
logger.info(f"Loaded rate limits for {len(self._tool_configs)} tools from manifest")
|
|
218
|
+
|
|
219
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
220
|
+
logger.error(f"Failed to load manifest: {e}")
|
|
221
|
+
|
|
222
|
+
def load_from_env(self) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Load rate limit overrides from environment variables.
|
|
225
|
+
|
|
226
|
+
Supports:
|
|
227
|
+
- FOUNDRY_RATE_LIMIT_DEFAULT: Default requests per minute
|
|
228
|
+
- FOUNDRY_RATE_LIMIT_BURST: Default burst limit
|
|
229
|
+
- FOUNDRY_RATE_LIMIT_{TOOL}: Per-tool override (e.g., FOUNDRY_RATE_LIMIT_RUN_TESTS=5)
|
|
230
|
+
"""
|
|
231
|
+
# Global defaults
|
|
232
|
+
default_rpm = os.environ.get("FOUNDRY_RATE_LIMIT_DEFAULT")
|
|
233
|
+
if default_rpm:
|
|
234
|
+
try:
|
|
235
|
+
self._global_config.requests_per_minute = int(default_rpm)
|
|
236
|
+
except ValueError:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
default_burst = os.environ.get("FOUNDRY_RATE_LIMIT_BURST")
|
|
240
|
+
if default_burst:
|
|
241
|
+
try:
|
|
242
|
+
self._global_config.burst_limit = int(default_burst)
|
|
243
|
+
except ValueError:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
# Per-tool overrides
|
|
247
|
+
for key, value in os.environ.items():
|
|
248
|
+
if key.startswith("FOUNDRY_RATE_LIMIT_") and key not in ("FOUNDRY_RATE_LIMIT_DEFAULT", "FOUNDRY_RATE_LIMIT_BURST"):
|
|
249
|
+
tool_name = key[19:].lower().replace("_", "_") # Keep underscores
|
|
250
|
+
try:
|
|
251
|
+
rpm = int(value)
|
|
252
|
+
if tool_name not in self._tool_configs:
|
|
253
|
+
self._tool_configs[tool_name] = RateLimitConfig()
|
|
254
|
+
self._tool_configs[tool_name].requests_per_minute = rpm
|
|
255
|
+
except ValueError:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
def get_limiter(self, tool_name: str, tenant_id: Optional[str] = None) -> TokenBucketLimiter:
|
|
259
|
+
"""
|
|
260
|
+
Get or create a rate limiter for a tool.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
tool_name: Name of the tool
|
|
264
|
+
tenant_id: Optional tenant ID for per-tenant limiting
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
TokenBucketLimiter for the tool
|
|
268
|
+
"""
|
|
269
|
+
if tenant_id:
|
|
270
|
+
if tool_name not in self._tenant_limiters[tenant_id]:
|
|
271
|
+
config = self._tool_configs.get(tool_name, self._global_config)
|
|
272
|
+
self._tenant_limiters[tenant_id][tool_name] = TokenBucketLimiter(config)
|
|
273
|
+
return self._tenant_limiters[tenant_id][tool_name]
|
|
274
|
+
|
|
275
|
+
if tool_name not in self._limiters:
|
|
276
|
+
config = self._tool_configs.get(tool_name, self._global_config)
|
|
277
|
+
self._limiters[tool_name] = TokenBucketLimiter(config)
|
|
278
|
+
return self._limiters[tool_name]
|
|
279
|
+
|
|
280
|
+
def check_limit(
|
|
281
|
+
self,
|
|
282
|
+
tool_name: str,
|
|
283
|
+
tenant_id: Optional[str] = None,
|
|
284
|
+
log_on_throttle: bool = True
|
|
285
|
+
) -> RateLimitResult:
|
|
286
|
+
"""
|
|
287
|
+
Check and enforce rate limit for a tool invocation.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
tool_name: Name of the tool being invoked
|
|
291
|
+
tenant_id: Optional tenant ID
|
|
292
|
+
log_on_throttle: Whether to log throttle events
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
RateLimitResult indicating if request is allowed
|
|
296
|
+
"""
|
|
297
|
+
limiter = self.get_limiter(tool_name, tenant_id)
|
|
298
|
+
result = limiter.acquire()
|
|
299
|
+
|
|
300
|
+
if not result.allowed and log_on_throttle:
|
|
301
|
+
self._log_throttle(tool_name, tenant_id, result)
|
|
302
|
+
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
def _log_throttle(
|
|
306
|
+
self,
|
|
307
|
+
tool_name: str,
|
|
308
|
+
tenant_id: Optional[str],
|
|
309
|
+
result: RateLimitResult
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Log a throttle event."""
|
|
312
|
+
audit_log(
|
|
313
|
+
"rate_limit_exceeded",
|
|
314
|
+
tool=tool_name,
|
|
315
|
+
tenant_id=tenant_id,
|
|
316
|
+
limit=result.limit,
|
|
317
|
+
reset_in=result.reset_in,
|
|
318
|
+
reason=result.reason,
|
|
319
|
+
success=False,
|
|
320
|
+
)
|
|
321
|
+
logger.warning(
|
|
322
|
+
f"Rate limit exceeded for {tool_name}"
|
|
323
|
+
+ (f" (tenant: {tenant_id})" if tenant_id else "")
|
|
324
|
+
+ f": {result.reason}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def log_auth_failure(
|
|
328
|
+
self,
|
|
329
|
+
tool_name: str,
|
|
330
|
+
tenant_id: Optional[str] = None,
|
|
331
|
+
reason: str = "Authentication failed"
|
|
332
|
+
) -> None:
|
|
333
|
+
"""
|
|
334
|
+
Log an authentication failure.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
tool_name: Tool that was accessed
|
|
338
|
+
tenant_id: Optional tenant ID
|
|
339
|
+
reason: Failure reason
|
|
340
|
+
"""
|
|
341
|
+
audit_log(
|
|
342
|
+
"auth_failure",
|
|
343
|
+
tool=tool_name,
|
|
344
|
+
tenant_id=tenant_id,
|
|
345
|
+
reason=reason,
|
|
346
|
+
success=False,
|
|
347
|
+
)
|
|
348
|
+
logger.warning(
|
|
349
|
+
f"Auth failure for {tool_name}"
|
|
350
|
+
+ (f" (tenant: {tenant_id})" if tenant_id else "")
|
|
351
|
+
+ f": {reason}"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def get_all_stats(self) -> Dict[str, Any]:
|
|
355
|
+
"""Get statistics for all limiters."""
|
|
356
|
+
stats = {
|
|
357
|
+
"schema_version": SCHEMA_VERSION,
|
|
358
|
+
"global_config": {
|
|
359
|
+
"requests_per_minute": self._global_config.requests_per_minute,
|
|
360
|
+
"burst_limit": self._global_config.burst_limit,
|
|
361
|
+
},
|
|
362
|
+
"tools": {},
|
|
363
|
+
"tenants": {},
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for tool_name, limiter in self._limiters.items():
|
|
367
|
+
stats["tools"][tool_name] = limiter.get_stats()
|
|
368
|
+
|
|
369
|
+
for tenant_id, limiters in self._tenant_limiters.items():
|
|
370
|
+
stats["tenants"][tenant_id] = {
|
|
371
|
+
name: limiter.get_stats()
|
|
372
|
+
for name, limiter in limiters.items()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return stats
|
|
376
|
+
|
|
377
|
+
def reset(self, tool_name: Optional[str] = None, tenant_id: Optional[str] = None) -> None:
|
|
378
|
+
"""
|
|
379
|
+
Reset rate limit state.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
tool_name: Specific tool to reset (all if None)
|
|
383
|
+
tenant_id: Specific tenant to reset (all if None)
|
|
384
|
+
"""
|
|
385
|
+
if tool_name and tenant_id:
|
|
386
|
+
if tenant_id in self._tenant_limiters and tool_name in self._tenant_limiters[tenant_id]:
|
|
387
|
+
del self._tenant_limiters[tenant_id][tool_name]
|
|
388
|
+
elif tool_name:
|
|
389
|
+
if tool_name in self._limiters:
|
|
390
|
+
del self._limiters[tool_name]
|
|
391
|
+
elif tenant_id:
|
|
392
|
+
if tenant_id in self._tenant_limiters:
|
|
393
|
+
del self._tenant_limiters[tenant_id]
|
|
394
|
+
else:
|
|
395
|
+
self._limiters.clear()
|
|
396
|
+
self._tenant_limiters.clear()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# Global instance
|
|
400
|
+
_manager: Optional[RateLimitManager] = None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def get_rate_limit_manager() -> RateLimitManager:
|
|
404
|
+
"""Get the global rate limit manager."""
|
|
405
|
+
global _manager
|
|
406
|
+
if _manager is None:
|
|
407
|
+
_manager = RateLimitManager()
|
|
408
|
+
_manager.load_from_manifest()
|
|
409
|
+
_manager.load_from_env()
|
|
410
|
+
return _manager
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def check_rate_limit(
|
|
414
|
+
tool_name: str,
|
|
415
|
+
tenant_id: Optional[str] = None
|
|
416
|
+
) -> RateLimitResult:
|
|
417
|
+
"""
|
|
418
|
+
Check rate limit for a tool invocation.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
tool_name: Name of the tool
|
|
422
|
+
tenant_id: Optional tenant ID
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
RateLimitResult indicating if request is allowed
|
|
426
|
+
"""
|
|
427
|
+
return get_rate_limit_manager().check_limit(tool_name, tenant_id)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Research workflows for multi-model orchestration.
|
|
2
|
+
|
|
3
|
+
This package provides conversation threading, multi-model consensus,
|
|
4
|
+
hypothesis-driven investigation, and creative brainstorming workflows.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from foundry_mcp.core.research.models import (
|
|
8
|
+
ConfidenceLevel,
|
|
9
|
+
ConsensusConfig,
|
|
10
|
+
ConsensusState,
|
|
11
|
+
ConsensusStrategy,
|
|
12
|
+
ConversationMessage,
|
|
13
|
+
ConversationThread,
|
|
14
|
+
Hypothesis,
|
|
15
|
+
Idea,
|
|
16
|
+
IdeaCluster,
|
|
17
|
+
IdeationPhase,
|
|
18
|
+
IdeationState,
|
|
19
|
+
InvestigationStep,
|
|
20
|
+
ModelResponse,
|
|
21
|
+
ThreadStatus,
|
|
22
|
+
ThinkDeepState,
|
|
23
|
+
WorkflowType,
|
|
24
|
+
)
|
|
25
|
+
from foundry_mcp.core.research.memory import (
|
|
26
|
+
FileStorageBackend,
|
|
27
|
+
ResearchMemory,
|
|
28
|
+
)
|
|
29
|
+
from foundry_mcp.core.research.workflows import (
|
|
30
|
+
ChatWorkflow,
|
|
31
|
+
ConsensusWorkflow,
|
|
32
|
+
IdeateWorkflow,
|
|
33
|
+
ResearchWorkflowBase,
|
|
34
|
+
ThinkDeepWorkflow,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Enums
|
|
39
|
+
"WorkflowType",
|
|
40
|
+
"ConfidenceLevel",
|
|
41
|
+
"ConsensusStrategy",
|
|
42
|
+
"ThreadStatus",
|
|
43
|
+
"IdeationPhase",
|
|
44
|
+
# Conversation models
|
|
45
|
+
"ConversationMessage",
|
|
46
|
+
"ConversationThread",
|
|
47
|
+
# THINKDEEP models
|
|
48
|
+
"Hypothesis",
|
|
49
|
+
"InvestigationStep",
|
|
50
|
+
"ThinkDeepState",
|
|
51
|
+
# IDEATE models
|
|
52
|
+
"Idea",
|
|
53
|
+
"IdeaCluster",
|
|
54
|
+
"IdeationState",
|
|
55
|
+
# CONSENSUS models
|
|
56
|
+
"ModelResponse",
|
|
57
|
+
"ConsensusConfig",
|
|
58
|
+
"ConsensusState",
|
|
59
|
+
# Storage
|
|
60
|
+
"FileStorageBackend",
|
|
61
|
+
"ResearchMemory",
|
|
62
|
+
# Workflows
|
|
63
|
+
"ResearchWorkflowBase",
|
|
64
|
+
"ChatWorkflow",
|
|
65
|
+
"ConsensusWorkflow",
|
|
66
|
+
"ThinkDeepWorkflow",
|
|
67
|
+
"IdeateWorkflow",
|
|
68
|
+
]
|