crca 1.4.0__py3-none-any.whl → 1.5.0__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.
- CRCA.py +172 -7
- MODEL_CARD.md +53 -0
- PKG-INFO +8 -2
- RELEASE_NOTES.md +17 -0
- STABILITY.md +19 -0
- architecture/hybrid/consistency_engine.py +362 -0
- architecture/hybrid/conversation_manager.py +421 -0
- architecture/hybrid/explanation_generator.py +452 -0
- architecture/hybrid/few_shot_learner.py +533 -0
- architecture/hybrid/graph_compressor.py +286 -0
- architecture/hybrid/hybrid_agent.py +4398 -0
- architecture/hybrid/language_compiler.py +623 -0
- architecture/hybrid/main,py +0 -0
- architecture/hybrid/reasoning_tracker.py +322 -0
- architecture/hybrid/self_verifier.py +524 -0
- architecture/hybrid/task_decomposer.py +567 -0
- architecture/hybrid/text_corrector.py +341 -0
- benchmark_results/crca_core_benchmarks.json +178 -0
- branches/crca_sd/crca_sd_realtime.py +6 -2
- branches/general_agent/__init__.py +102 -0
- branches/general_agent/general_agent.py +1400 -0
- branches/general_agent/personality.py +169 -0
- branches/general_agent/utils/__init__.py +19 -0
- branches/general_agent/utils/prompt_builder.py +170 -0
- {crca-1.4.0.dist-info → crca-1.5.0.dist-info}/METADATA +8 -2
- {crca-1.4.0.dist-info → crca-1.5.0.dist-info}/RECORD +303 -20
- crca_core/__init__.py +35 -0
- crca_core/benchmarks/__init__.py +14 -0
- crca_core/benchmarks/synthetic_scm.py +103 -0
- crca_core/core/__init__.py +23 -0
- crca_core/core/api.py +120 -0
- crca_core/core/estimate.py +208 -0
- crca_core/core/godclass.py +72 -0
- crca_core/core/intervention_design.py +174 -0
- crca_core/core/lifecycle.py +48 -0
- crca_core/discovery/__init__.py +9 -0
- crca_core/discovery/tabular.py +193 -0
- crca_core/identify/__init__.py +171 -0
- crca_core/identify/backdoor.py +39 -0
- crca_core/identify/frontdoor.py +48 -0
- crca_core/identify/graph.py +106 -0
- crca_core/identify/id_algorithm.py +43 -0
- crca_core/identify/iv.py +48 -0
- crca_core/models/__init__.py +67 -0
- crca_core/models/provenance.py +56 -0
- crca_core/models/refusal.py +39 -0
- crca_core/models/result.py +83 -0
- crca_core/models/spec.py +151 -0
- crca_core/models/validation.py +68 -0
- crca_core/scm/__init__.py +9 -0
- crca_core/scm/linear_gaussian.py +198 -0
- crca_core/timeseries/__init__.py +6 -0
- crca_core/timeseries/pcmci.py +181 -0
- crca_llm/__init__.py +12 -0
- crca_llm/client.py +85 -0
- crca_llm/coauthor.py +118 -0
- crca_llm/orchestrator.py +289 -0
- crca_llm/types.py +21 -0
- crca_reasoning/__init__.py +16 -0
- crca_reasoning/critique.py +54 -0
- crca_reasoning/godclass.py +206 -0
- crca_reasoning/memory.py +24 -0
- crca_reasoning/rationale.py +10 -0
- crca_reasoning/react_controller.py +81 -0
- crca_reasoning/tool_router.py +97 -0
- crca_reasoning/types.py +40 -0
- crca_sd/__init__.py +15 -0
- crca_sd/crca_sd_core.py +2 -0
- crca_sd/crca_sd_governance.py +2 -0
- crca_sd/crca_sd_mpc.py +2 -0
- crca_sd/crca_sd_realtime.py +2 -0
- crca_sd/crca_sd_tui.py +2 -0
- cuda-keyring_1.1-1_all.deb +0 -0
- cuda-keyring_1.1-1_all.deb.1 +0 -0
- docs/IMAGE_ANNOTATION_USAGE.md +539 -0
- docs/INSTALL_DEEPSPEED.md +125 -0
- docs/api/branches/crca-cg.md +19 -0
- docs/api/branches/crca-q.md +27 -0
- docs/api/branches/crca-sd.md +37 -0
- docs/api/branches/general-agent.md +24 -0
- docs/api/branches/overview.md +19 -0
- docs/api/crca/agent-methods.md +62 -0
- docs/api/crca/operations.md +79 -0
- docs/api/crca/overview.md +32 -0
- docs/api/image-annotation/engine.md +52 -0
- docs/api/image-annotation/overview.md +17 -0
- docs/api/schemas/annotation.md +34 -0
- docs/api/schemas/core-schemas.md +82 -0
- docs/api/schemas/overview.md +32 -0
- docs/api/schemas/policy.md +30 -0
- docs/api/utils/conversation.md +22 -0
- docs/api/utils/graph-reasoner.md +32 -0
- docs/api/utils/overview.md +21 -0
- docs/api/utils/router.md +19 -0
- docs/api/utils/utilities.md +97 -0
- docs/architecture/causal-graphs.md +41 -0
- docs/architecture/data-flow.md +29 -0
- docs/architecture/design-principles.md +33 -0
- docs/architecture/hybrid-agent/components.md +38 -0
- docs/architecture/hybrid-agent/consistency.md +26 -0
- docs/architecture/hybrid-agent/overview.md +44 -0
- docs/architecture/hybrid-agent/reasoning.md +22 -0
- docs/architecture/llm-integration.md +26 -0
- docs/architecture/modular-structure.md +37 -0
- docs/architecture/overview.md +69 -0
- docs/architecture/policy-engine-arch.md +29 -0
- docs/branches/crca-cg/corposwarm.md +39 -0
- docs/branches/crca-cg/esg-scoring.md +30 -0
- docs/branches/crca-cg/multi-agent.md +35 -0
- docs/branches/crca-cg/overview.md +40 -0
- docs/branches/crca-q/alternative-data.md +55 -0
- docs/branches/crca-q/architecture.md +71 -0
- docs/branches/crca-q/backtesting.md +45 -0
- docs/branches/crca-q/causal-engine.md +33 -0
- docs/branches/crca-q/execution.md +39 -0
- docs/branches/crca-q/market-data.md +60 -0
- docs/branches/crca-q/overview.md +58 -0
- docs/branches/crca-q/philosophy.md +60 -0
- docs/branches/crca-q/portfolio-optimization.md +66 -0
- docs/branches/crca-q/risk-management.md +102 -0
- docs/branches/crca-q/setup.md +65 -0
- docs/branches/crca-q/signal-generation.md +61 -0
- docs/branches/crca-q/signal-validation.md +43 -0
- docs/branches/crca-sd/core.md +84 -0
- docs/branches/crca-sd/governance.md +53 -0
- docs/branches/crca-sd/mpc-solver.md +65 -0
- docs/branches/crca-sd/overview.md +59 -0
- docs/branches/crca-sd/realtime.md +28 -0
- docs/branches/crca-sd/tui.md +20 -0
- docs/branches/general-agent/overview.md +37 -0
- docs/branches/general-agent/personality.md +36 -0
- docs/branches/general-agent/prompt-builder.md +30 -0
- docs/changelog/index.md +79 -0
- docs/contributing/code-style.md +69 -0
- docs/contributing/documentation.md +43 -0
- docs/contributing/overview.md +29 -0
- docs/contributing/testing.md +29 -0
- docs/core/crcagent/async-operations.md +65 -0
- docs/core/crcagent/automatic-extraction.md +107 -0
- docs/core/crcagent/batch-prediction.md +80 -0
- docs/core/crcagent/bayesian-inference.md +60 -0
- docs/core/crcagent/causal-graph.md +92 -0
- docs/core/crcagent/counterfactuals.md +96 -0
- docs/core/crcagent/deterministic-simulation.md +78 -0
- docs/core/crcagent/dual-mode-operation.md +82 -0
- docs/core/crcagent/initialization.md +88 -0
- docs/core/crcagent/optimization.md +65 -0
- docs/core/crcagent/overview.md +63 -0
- docs/core/crcagent/time-series.md +57 -0
- docs/core/schemas/annotation.md +30 -0
- docs/core/schemas/core-schemas.md +82 -0
- docs/core/schemas/overview.md +30 -0
- docs/core/schemas/policy.md +41 -0
- docs/core/templates/base-agent.md +31 -0
- docs/core/templates/feature-mixins.md +31 -0
- docs/core/templates/overview.md +29 -0
- docs/core/templates/templates-guide.md +75 -0
- docs/core/tools/mcp-client.md +34 -0
- docs/core/tools/overview.md +24 -0
- docs/core/utils/conversation.md +27 -0
- docs/core/utils/graph-reasoner.md +29 -0
- docs/core/utils/overview.md +27 -0
- docs/core/utils/router.md +27 -0
- docs/core/utils/utilities.md +97 -0
- docs/css/custom.css +84 -0
- docs/examples/basic-usage.md +57 -0
- docs/examples/general-agent/general-agent-examples.md +50 -0
- docs/examples/hybrid-agent/hybrid-agent-examples.md +56 -0
- docs/examples/image-annotation/image-annotation-examples.md +54 -0
- docs/examples/integration/integration-examples.md +58 -0
- docs/examples/overview.md +37 -0
- docs/examples/trading/trading-examples.md +46 -0
- docs/features/causal-reasoning/advanced-topics.md +101 -0
- docs/features/causal-reasoning/counterfactuals.md +43 -0
- docs/features/causal-reasoning/do-calculus.md +50 -0
- docs/features/causal-reasoning/overview.md +47 -0
- docs/features/causal-reasoning/structural-models.md +52 -0
- docs/features/hybrid-agent/advanced-components.md +55 -0
- docs/features/hybrid-agent/core-components.md +64 -0
- docs/features/hybrid-agent/overview.md +34 -0
- docs/features/image-annotation/engine.md +82 -0
- docs/features/image-annotation/features.md +113 -0
- docs/features/image-annotation/integration.md +75 -0
- docs/features/image-annotation/overview.md +53 -0
- docs/features/image-annotation/quickstart.md +73 -0
- docs/features/policy-engine/doctrine-ledger.md +105 -0
- docs/features/policy-engine/monitoring.md +44 -0
- docs/features/policy-engine/mpc-control.md +89 -0
- docs/features/policy-engine/overview.md +46 -0
- docs/getting-started/configuration.md +225 -0
- docs/getting-started/first-agent.md +164 -0
- docs/getting-started/installation.md +144 -0
- docs/getting-started/quickstart.md +137 -0
- docs/index.md +118 -0
- docs/js/mathjax.js +13 -0
- docs/lrm/discovery_proof_notes.md +25 -0
- docs/lrm/finetune_full.md +83 -0
- docs/lrm/math_appendix.md +120 -0
- docs/lrm/overview.md +32 -0
- docs/mkdocs.yml +238 -0
- docs/stylesheets/extra.css +21 -0
- docs_generated/crca_core/CounterfactualResult.md +12 -0
- docs_generated/crca_core/DiscoveryHypothesisResult.md +13 -0
- docs_generated/crca_core/DraftSpec.md +13 -0
- docs_generated/crca_core/EstimateResult.md +13 -0
- docs_generated/crca_core/IdentificationResult.md +17 -0
- docs_generated/crca_core/InterventionDesignResult.md +12 -0
- docs_generated/crca_core/LockedSpec.md +15 -0
- docs_generated/crca_core/RefusalResult.md +12 -0
- docs_generated/crca_core/ValidationReport.md +9 -0
- docs_generated/crca_core/index.md +13 -0
- examples/general_agent_example.py +277 -0
- examples/general_agent_quickstart.py +202 -0
- examples/general_agent_simple.py +92 -0
- examples/hybrid_agent_auto_extraction.py +84 -0
- examples/hybrid_agent_dictionary_demo.py +104 -0
- examples/hybrid_agent_enhanced.py +179 -0
- examples/hybrid_agent_general_knowledge.py +107 -0
- examples/image_annotation_quickstart.py +328 -0
- examples/test_hybrid_fixes.py +77 -0
- image_annotation/__init__.py +27 -0
- image_annotation/annotation_engine.py +2593 -0
- install_cuda_wsl2.sh +59 -0
- install_deepspeed.sh +56 -0
- install_deepspeed_simple.sh +87 -0
- mkdocs.yml +252 -0
- ollama/Modelfile +8 -0
- prompts/__init__.py +2 -1
- prompts/default_crca.py +9 -1
- prompts/general_agent.py +227 -0
- prompts/image_annotation.py +56 -0
- pyproject.toml +17 -2
- requirements-docs.txt +10 -0
- requirements.txt +21 -2
- schemas/__init__.py +26 -1
- schemas/annotation.py +222 -0
- schemas/conversation.py +193 -0
- schemas/hybrid.py +211 -0
- schemas/reasoning.py +276 -0
- schemas_export/crca_core/CounterfactualResult.schema.json +108 -0
- schemas_export/crca_core/DiscoveryHypothesisResult.schema.json +113 -0
- schemas_export/crca_core/DraftSpec.schema.json +635 -0
- schemas_export/crca_core/EstimateResult.schema.json +113 -0
- schemas_export/crca_core/IdentificationResult.schema.json +145 -0
- schemas_export/crca_core/InterventionDesignResult.schema.json +111 -0
- schemas_export/crca_core/LockedSpec.schema.json +646 -0
- schemas_export/crca_core/RefusalResult.schema.json +90 -0
- schemas_export/crca_core/ValidationReport.schema.json +62 -0
- scripts/build_lrm_dataset.py +80 -0
- scripts/export_crca_core_schemas.py +54 -0
- scripts/export_hf_lrm.py +37 -0
- scripts/export_ollama_gguf.py +45 -0
- scripts/generate_changelog.py +157 -0
- scripts/generate_crca_core_docs_from_schemas.py +86 -0
- scripts/run_crca_core_benchmarks.py +163 -0
- scripts/run_full_finetune.py +198 -0
- scripts/run_lrm_eval.py +31 -0
- templates/graph_management.py +29 -0
- tests/conftest.py +9 -0
- tests/test_core.py +2 -3
- tests/test_crca_core_discovery_tabular.py +15 -0
- tests/test_crca_core_estimate_dowhy.py +36 -0
- tests/test_crca_core_identify.py +18 -0
- tests/test_crca_core_intervention_design.py +36 -0
- tests/test_crca_core_linear_gaussian_scm.py +69 -0
- tests/test_crca_core_spec.py +25 -0
- tests/test_crca_core_timeseries_pcmci.py +15 -0
- tests/test_crca_llm_coauthor.py +12 -0
- tests/test_crca_llm_orchestrator.py +80 -0
- tests/test_hybrid_agent_llm_enhanced.py +556 -0
- tests/test_image_annotation_demo.py +376 -0
- tests/test_image_annotation_operational.py +408 -0
- tests/test_image_annotation_unit.py +551 -0
- tests/test_training_moe.py +13 -0
- training/__init__.py +42 -0
- training/datasets.py +140 -0
- training/deepspeed_zero2_0_5b.json +22 -0
- training/deepspeed_zero2_1_5b.json +22 -0
- training/deepspeed_zero3_0_5b.json +28 -0
- training/deepspeed_zero3_14b.json +28 -0
- training/deepspeed_zero3_h100_3gpu.json +20 -0
- training/deepspeed_zero3_offload.json +28 -0
- training/eval.py +92 -0
- training/finetune.py +516 -0
- training/public_datasets.py +89 -0
- training_data/react_train.jsonl +7473 -0
- utils/agent_discovery.py +311 -0
- utils/batch_processor.py +317 -0
- utils/conversation.py +78 -0
- utils/edit_distance.py +118 -0
- utils/formatter.py +33 -0
- utils/graph_reasoner.py +530 -0
- utils/rate_limiter.py +283 -0
- utils/router.py +2 -2
- utils/tool_discovery.py +307 -0
- webui/__init__.py +10 -0
- webui/app.py +229 -0
- webui/config.py +104 -0
- webui/static/css/style.css +332 -0
- webui/static/js/main.js +284 -0
- webui/templates/index.html +42 -0
- tests/test_crca_excel.py +0 -166
- tests/test_data_broker.py +0 -424
- tests/test_palantir.py +0 -349
- {crca-1.4.0.dist-info → crca-1.5.0.dist-info}/WHEEL +0 -0
- {crca-1.4.0.dist-info → crca-1.5.0.dist-info}/licenses/LICENSE +0 -0
utils/rate_limiter.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rate limiting utility module.
|
|
3
|
+
|
|
4
|
+
Provides rate limiting functionality for agents and API calls with support for:
|
|
5
|
+
- Token-based and request-based limiting
|
|
6
|
+
- Per-user/session rate limits
|
|
7
|
+
- Queue management for rate-limited requests
|
|
8
|
+
- Configurable limits (RPM, RPH)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
import threading
|
|
13
|
+
from collections import deque, defaultdict
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Dict, Optional, Any, Tuple
|
|
16
|
+
from loguru import logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RateLimitConfig:
|
|
21
|
+
"""Configuration for rate limiting.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
requests_per_minute: Maximum requests per minute (0 = unlimited)
|
|
25
|
+
requests_per_hour: Maximum requests per hour (0 = unlimited)
|
|
26
|
+
tokens_per_minute: Maximum tokens per minute (0 = unlimited)
|
|
27
|
+
tokens_per_hour: Maximum tokens per hour (0 = unlimited)
|
|
28
|
+
queue_enabled: Whether to queue requests when rate limited
|
|
29
|
+
max_queue_size: Maximum queue size
|
|
30
|
+
"""
|
|
31
|
+
requests_per_minute: int = 60
|
|
32
|
+
requests_per_hour: int = 1000
|
|
33
|
+
tokens_per_minute: int = 0 # 0 = unlimited
|
|
34
|
+
tokens_per_hour: int = 0 # 0 = unlimited
|
|
35
|
+
queue_enabled: bool = True
|
|
36
|
+
max_queue_size: int = 1000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RateLimiter:
|
|
40
|
+
"""Rate limiter with per-user/session tracking.
|
|
41
|
+
|
|
42
|
+
Provides rate limiting functionality with support for:
|
|
43
|
+
- Request-based limiting (RPM, RPH)
|
|
44
|
+
- Token-based limiting (TPM, TPH)
|
|
45
|
+
- Per-user/session tracking
|
|
46
|
+
- Queue management for rate-limited requests
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, config: Optional[RateLimitConfig] = None):
|
|
50
|
+
"""Initialize rate limiter.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
config: Rate limit configuration. Uses defaults if None.
|
|
54
|
+
"""
|
|
55
|
+
self.config = config or RateLimitConfig()
|
|
56
|
+
self._lock = threading.RLock()
|
|
57
|
+
|
|
58
|
+
# Per-user tracking: {user_id: {type: deque of timestamps}}
|
|
59
|
+
self._request_history: Dict[str, Dict[str, deque]] = defaultdict(
|
|
60
|
+
lambda: {
|
|
61
|
+
"minute": deque(maxlen=self.config.requests_per_minute * 2),
|
|
62
|
+
"hour": deque(maxlen=self.config.requests_per_hour * 2),
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self._token_history: Dict[str, Dict[str, deque]] = defaultdict(
|
|
67
|
+
lambda: {
|
|
68
|
+
"minute": deque(maxlen=1000),
|
|
69
|
+
"hour": deque(maxlen=10000),
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Queue for rate-limited requests
|
|
74
|
+
self._queues: Dict[str, deque] = defaultdict(lambda: deque(maxlen=self.config.max_queue_size))
|
|
75
|
+
|
|
76
|
+
logger.debug(f"Initialized RateLimiter with config: {self.config}")
|
|
77
|
+
|
|
78
|
+
def _clean_old_entries(self, user_id: str, window: str) -> None:
|
|
79
|
+
"""Remove old entries outside the time window.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
user_id: User identifier
|
|
83
|
+
window: Time window ('minute' or 'hour')
|
|
84
|
+
"""
|
|
85
|
+
current_time = time.time()
|
|
86
|
+
cutoff = current_time - (60 if window == "minute" else 3600)
|
|
87
|
+
|
|
88
|
+
# Clean request history
|
|
89
|
+
if user_id in self._request_history:
|
|
90
|
+
history = self._request_history[user_id][window]
|
|
91
|
+
while history and history[0] < cutoff:
|
|
92
|
+
history.popleft()
|
|
93
|
+
|
|
94
|
+
# Clean token history
|
|
95
|
+
if user_id in self._token_history:
|
|
96
|
+
history = self._token_history[user_id][window]
|
|
97
|
+
while history and len(history) > 0:
|
|
98
|
+
if history[0][0] < cutoff:
|
|
99
|
+
history.popleft()
|
|
100
|
+
else:
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
def check_rate_limit(
|
|
104
|
+
self,
|
|
105
|
+
user_id: str = "default",
|
|
106
|
+
token_count: int = 0,
|
|
107
|
+
) -> Tuple[bool, Optional[str]]:
|
|
108
|
+
"""Check if request is within rate limits.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
user_id: User identifier for per-user tracking
|
|
112
|
+
token_count: Number of tokens in the request (0 if not applicable)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Tuple of (is_allowed, error_message)
|
|
116
|
+
- is_allowed: True if request is allowed, False otherwise
|
|
117
|
+
- error_message: Error message if not allowed, None otherwise
|
|
118
|
+
"""
|
|
119
|
+
with self._lock:
|
|
120
|
+
current_time = time.time()
|
|
121
|
+
|
|
122
|
+
# Clean old entries
|
|
123
|
+
self._clean_old_entries(user_id, "minute")
|
|
124
|
+
self._clean_old_entries(user_id, "hour")
|
|
125
|
+
|
|
126
|
+
# Check request-based limits
|
|
127
|
+
if self.config.requests_per_minute > 0:
|
|
128
|
+
minute_requests = len(self._request_history[user_id]["minute"])
|
|
129
|
+
if minute_requests >= self.config.requests_per_minute:
|
|
130
|
+
return False, f"Rate limit exceeded: {minute_requests}/{self.config.requests_per_minute} requests per minute"
|
|
131
|
+
|
|
132
|
+
if self.config.requests_per_hour > 0:
|
|
133
|
+
hour_requests = len(self._request_history[user_id]["hour"])
|
|
134
|
+
if hour_requests >= self.config.requests_per_hour:
|
|
135
|
+
return False, f"Rate limit exceeded: {hour_requests}/{self.config.requests_per_hour} requests per hour"
|
|
136
|
+
|
|
137
|
+
# Check token-based limits
|
|
138
|
+
if token_count > 0:
|
|
139
|
+
if self.config.tokens_per_minute > 0:
|
|
140
|
+
minute_tokens = sum(
|
|
141
|
+
tokens for _, tokens in self._token_history[user_id]["minute"]
|
|
142
|
+
)
|
|
143
|
+
if minute_tokens + token_count > self.config.tokens_per_minute:
|
|
144
|
+
return False, f"Token limit exceeded: {minute_tokens + token_count}/{self.config.tokens_per_minute} tokens per minute"
|
|
145
|
+
|
|
146
|
+
if self.config.tokens_per_hour > 0:
|
|
147
|
+
hour_tokens = sum(
|
|
148
|
+
tokens for _, tokens in self._token_history[user_id]["hour"]
|
|
149
|
+
)
|
|
150
|
+
if hour_tokens + token_count > self.config.tokens_per_hour:
|
|
151
|
+
return False, f"Token limit exceeded: {hour_tokens + token_count}/{self.config.tokens_per_hour} tokens per hour"
|
|
152
|
+
|
|
153
|
+
# Record request
|
|
154
|
+
self._request_history[user_id]["minute"].append(current_time)
|
|
155
|
+
self._request_history[user_id]["hour"].append(current_time)
|
|
156
|
+
|
|
157
|
+
# Record tokens if applicable
|
|
158
|
+
if token_count > 0:
|
|
159
|
+
self._token_history[user_id]["minute"].append((current_time, token_count))
|
|
160
|
+
self._token_history[user_id]["hour"].append((current_time, token_count))
|
|
161
|
+
|
|
162
|
+
return True, None
|
|
163
|
+
|
|
164
|
+
def wait_if_rate_limited(
|
|
165
|
+
self,
|
|
166
|
+
user_id: str = "default",
|
|
167
|
+
token_count: int = 0,
|
|
168
|
+
max_wait: float = 60.0,
|
|
169
|
+
) -> bool:
|
|
170
|
+
"""Wait if rate limited, up to max_wait seconds.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
user_id: User identifier
|
|
174
|
+
token_count: Number of tokens in the request
|
|
175
|
+
max_wait: Maximum time to wait in seconds
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if request can proceed, False if still rate limited after waiting
|
|
179
|
+
"""
|
|
180
|
+
start_time = time.time()
|
|
181
|
+
|
|
182
|
+
while time.time() - start_time < max_wait:
|
|
183
|
+
is_allowed, error_msg = self.check_rate_limit(user_id, token_count)
|
|
184
|
+
if is_allowed:
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
# Calculate wait time
|
|
188
|
+
if "minute" in error_msg:
|
|
189
|
+
# Wait until oldest request in minute window expires
|
|
190
|
+
if self._request_history[user_id]["minute"]:
|
|
191
|
+
oldest = self._request_history[user_id]["minute"][0]
|
|
192
|
+
wait_time = min(60 - (time.time() - oldest) + 1, max_wait)
|
|
193
|
+
if wait_time > 0:
|
|
194
|
+
time.sleep(min(wait_time, 1.0)) # Sleep in 1s increments
|
|
195
|
+
else:
|
|
196
|
+
time.sleep(0.1)
|
|
197
|
+
elif "hour" in error_msg:
|
|
198
|
+
# Wait until oldest request in hour window expires
|
|
199
|
+
if self._request_history[user_id]["hour"]:
|
|
200
|
+
oldest = self._request_history[user_id]["hour"][0]
|
|
201
|
+
wait_time = min(3600 - (time.time() - oldest) + 1, max_wait)
|
|
202
|
+
if wait_time > 0:
|
|
203
|
+
time.sleep(min(wait_time, 1.0))
|
|
204
|
+
else:
|
|
205
|
+
time.sleep(0.1)
|
|
206
|
+
else:
|
|
207
|
+
time.sleep(0.1)
|
|
208
|
+
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
def get_rate_limit_status(self, user_id: str = "default") -> Dict[str, Any]:
|
|
212
|
+
"""Get current rate limit status for a user.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
user_id: User identifier
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Dictionary with rate limit status information
|
|
219
|
+
"""
|
|
220
|
+
with self._lock:
|
|
221
|
+
self._clean_old_entries(user_id, "minute")
|
|
222
|
+
self._clean_old_entries(user_id, "hour")
|
|
223
|
+
|
|
224
|
+
minute_requests = len(self._request_history[user_id]["minute"])
|
|
225
|
+
hour_requests = len(self._request_history[user_id]["hour"])
|
|
226
|
+
|
|
227
|
+
minute_tokens = sum(
|
|
228
|
+
tokens for _, tokens in self._token_history[user_id]["minute"]
|
|
229
|
+
)
|
|
230
|
+
hour_tokens = sum(
|
|
231
|
+
tokens for _, tokens in self._token_history[user_id]["hour"]
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"user_id": user_id,
|
|
236
|
+
"requests": {
|
|
237
|
+
"minute": {
|
|
238
|
+
"current": minute_requests,
|
|
239
|
+
"limit": self.config.requests_per_minute,
|
|
240
|
+
"remaining": max(0, self.config.requests_per_minute - minute_requests),
|
|
241
|
+
},
|
|
242
|
+
"hour": {
|
|
243
|
+
"current": hour_requests,
|
|
244
|
+
"limit": self.config.requests_per_hour,
|
|
245
|
+
"remaining": max(0, self.config.requests_per_hour - hour_requests),
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
"tokens": {
|
|
249
|
+
"minute": {
|
|
250
|
+
"current": minute_tokens,
|
|
251
|
+
"limit": self.config.tokens_per_minute,
|
|
252
|
+
"remaining": max(0, self.config.tokens_per_minute - minute_tokens) if self.config.tokens_per_minute > 0 else -1,
|
|
253
|
+
},
|
|
254
|
+
"hour": {
|
|
255
|
+
"current": hour_tokens,
|
|
256
|
+
"limit": self.config.tokens_per_hour,
|
|
257
|
+
"remaining": max(0, self.config.tokens_per_hour - hour_tokens) if self.config.tokens_per_hour > 0 else -1,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
def reset_user(self, user_id: str) -> None:
|
|
263
|
+
"""Reset rate limit tracking for a user.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
user_id: User identifier
|
|
267
|
+
"""
|
|
268
|
+
with self._lock:
|
|
269
|
+
if user_id in self._request_history:
|
|
270
|
+
del self._request_history[user_id]
|
|
271
|
+
if user_id in self._token_history:
|
|
272
|
+
del self._token_history[user_id]
|
|
273
|
+
if user_id in self._queues:
|
|
274
|
+
del self._queues[user_id]
|
|
275
|
+
logger.debug(f"Reset rate limit tracking for user: {user_id}")
|
|
276
|
+
|
|
277
|
+
def reset_all(self) -> None:
|
|
278
|
+
"""Reset rate limit tracking for all users."""
|
|
279
|
+
with self._lock:
|
|
280
|
+
self._request_history.clear()
|
|
281
|
+
self._token_history.clear()
|
|
282
|
+
self._queues.clear()
|
|
283
|
+
logger.debug("Reset rate limit tracking for all users")
|
utils/router.py
CHANGED
|
@@ -24,7 +24,7 @@ from swarms.structs.concurrent_workflow import ConcurrentWorkflow
|
|
|
24
24
|
from swarms.structs.council_as_judge import CouncilAsAJudge
|
|
25
25
|
# DebateWithJudge may not be available - make optional
|
|
26
26
|
try:
|
|
27
|
-
from swarms.structs.debate_with_judge import DebateWithJudge
|
|
27
|
+
from swarms.structs.debate_with_judge import DebateWithJudge
|
|
28
28
|
except ImportError:
|
|
29
29
|
DebateWithJudge = None # Will be handled in factory
|
|
30
30
|
from swarms.structs.groupchat import GroupChat
|
|
@@ -43,7 +43,7 @@ from swarms.utils.loguru_logger import initialize_logger
|
|
|
43
43
|
from swarms.utils.output_types import OutputType
|
|
44
44
|
# LLMCouncil may not be available - make optional
|
|
45
45
|
try:
|
|
46
|
-
from swarms.structs.llm_council import LLMCouncil
|
|
46
|
+
from swarms.structs.llm_council import LLMCouncil
|
|
47
47
|
except ImportError:
|
|
48
48
|
LLMCouncil = None # Will be handled in factory
|
|
49
49
|
from swarms.structs.round_robin import RoundRobinSwarm
|
utils/tool_discovery.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic tool discovery utilities.
|
|
3
|
+
|
|
4
|
+
Provides functionality for:
|
|
5
|
+
- Tool registry and scanning
|
|
6
|
+
- Tool metadata extraction
|
|
7
|
+
- Tool schema generation
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from swarms.tools.tool import convert_function_to_openai_function_schema
|
|
16
|
+
TOOL_CONVERSION_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
TOOL_CONVERSION_AVAILABLE = False
|
|
19
|
+
logger.debug("Tool conversion utilities not available")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ToolRegistry:
|
|
23
|
+
"""Registry for managing and discovering tools."""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
"""Initialize tool registry."""
|
|
27
|
+
self._tools: Dict[str, Callable] = {}
|
|
28
|
+
self._tool_metadata: Dict[str, Dict[str, Any]] = {}
|
|
29
|
+
logger.debug("Initialized ToolRegistry")
|
|
30
|
+
|
|
31
|
+
def register_tool(
|
|
32
|
+
self,
|
|
33
|
+
tool: Callable,
|
|
34
|
+
name: Optional[str] = None,
|
|
35
|
+
description: Optional[str] = None,
|
|
36
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
37
|
+
) -> str:
|
|
38
|
+
"""Register a tool in the registry.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
tool: Callable tool function
|
|
42
|
+
name: Optional tool name (uses function name if None)
|
|
43
|
+
description: Optional tool description
|
|
44
|
+
metadata: Optional additional metadata
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Registered tool name
|
|
48
|
+
"""
|
|
49
|
+
tool_name = name or tool.__name__
|
|
50
|
+
|
|
51
|
+
if tool_name in self._tools:
|
|
52
|
+
logger.warning(f"Tool '{tool_name}' already registered, overwriting")
|
|
53
|
+
|
|
54
|
+
self._tools[tool_name] = tool
|
|
55
|
+
|
|
56
|
+
# Extract metadata
|
|
57
|
+
tool_metadata = {
|
|
58
|
+
"name": tool_name,
|
|
59
|
+
"description": description or tool.__doc__ or "No description",
|
|
60
|
+
"function": tool,
|
|
61
|
+
"signature": str(inspect.signature(tool)),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if metadata:
|
|
65
|
+
tool_metadata.update(metadata)
|
|
66
|
+
|
|
67
|
+
self._tool_metadata[tool_name] = tool_metadata
|
|
68
|
+
|
|
69
|
+
logger.debug(f"Registered tool: {tool_name}")
|
|
70
|
+
return tool_name
|
|
71
|
+
|
|
72
|
+
def unregister_tool(self, name: str) -> bool:
|
|
73
|
+
"""Unregister a tool from the registry.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
name: Tool name to unregister
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True if tool was unregistered, False if not found
|
|
80
|
+
"""
|
|
81
|
+
if name in self._tools:
|
|
82
|
+
del self._tools[name]
|
|
83
|
+
if name in self._tool_metadata:
|
|
84
|
+
del self._tool_metadata[name]
|
|
85
|
+
logger.debug(f"Unregistered tool: {name}")
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def get_tool(self, name: str) -> Optional[Callable]:
|
|
90
|
+
"""Get a tool by name.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
name: Tool name
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Tool function or None if not found
|
|
97
|
+
"""
|
|
98
|
+
return self._tools.get(name)
|
|
99
|
+
|
|
100
|
+
def get_tool_metadata(self, name: str) -> Optional[Dict[str, Any]]:
|
|
101
|
+
"""Get tool metadata by name.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
name: Tool name
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Tool metadata dictionary or None if not found
|
|
108
|
+
"""
|
|
109
|
+
return self._tool_metadata.get(name)
|
|
110
|
+
|
|
111
|
+
def list_tools(self) -> List[str]:
|
|
112
|
+
"""List all registered tool names.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of tool names
|
|
116
|
+
"""
|
|
117
|
+
return list(self._tools.keys())
|
|
118
|
+
|
|
119
|
+
def get_all_tools(self) -> Dict[str, Callable]:
|
|
120
|
+
"""Get all registered tools.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dictionary mapping tool names to tool functions
|
|
124
|
+
"""
|
|
125
|
+
return self._tools.copy()
|
|
126
|
+
|
|
127
|
+
def get_all_metadata(self) -> Dict[str, Dict[str, Any]]:
|
|
128
|
+
"""Get metadata for all tools.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dictionary mapping tool names to metadata
|
|
132
|
+
"""
|
|
133
|
+
return self._tool_metadata.copy()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Global tool registry instance
|
|
137
|
+
_global_registry = ToolRegistry()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_global_registry() -> ToolRegistry:
|
|
141
|
+
"""Get the global tool registry instance.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Global ToolRegistry instance
|
|
145
|
+
"""
|
|
146
|
+
return _global_registry
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def discover_tools_in_module(module: Any) -> List[Callable]:
|
|
150
|
+
"""Discover all callable tools in a module.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
module: Module to scan for tools
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of discovered tool functions
|
|
157
|
+
"""
|
|
158
|
+
tools = []
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
for name in dir(module):
|
|
162
|
+
obj = getattr(module, name)
|
|
163
|
+
if callable(obj) and not name.startswith("_"):
|
|
164
|
+
# Check if it looks like a tool (has docstring, takes parameters, etc.)
|
|
165
|
+
if inspect.isfunction(obj) or inspect.ismethod(obj):
|
|
166
|
+
tools.append(obj)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Error discovering tools in module: {e}")
|
|
169
|
+
|
|
170
|
+
return tools
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def discover_tools_in_object(obj: Any) -> List[Callable]:
|
|
174
|
+
"""Discover all callable tools in an object.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
obj: Object to scan for tools
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of discovered tool methods
|
|
181
|
+
"""
|
|
182
|
+
tools = []
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
for name in dir(obj):
|
|
186
|
+
attr = getattr(obj, name)
|
|
187
|
+
if callable(attr) and not name.startswith("_"):
|
|
188
|
+
if inspect.ismethod(attr) or (inspect.isfunction(attr) and hasattr(obj, name)):
|
|
189
|
+
tools.append(attr)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Error discovering tools in object: {e}")
|
|
192
|
+
|
|
193
|
+
return tools
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def extract_tool_schema(tool: Callable) -> Optional[Dict[str, Any]]:
|
|
197
|
+
"""Extract OpenAI function schema from a tool.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
tool: Tool function to extract schema from
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
OpenAI function schema dictionary or None if extraction fails
|
|
204
|
+
"""
|
|
205
|
+
if not TOOL_CONVERSION_AVAILABLE:
|
|
206
|
+
# Fallback: create basic schema from function signature
|
|
207
|
+
try:
|
|
208
|
+
sig = inspect.signature(tool)
|
|
209
|
+
params = {}
|
|
210
|
+
for param_name, param in sig.parameters.items():
|
|
211
|
+
param_type = "string"
|
|
212
|
+
if param.annotation != inspect.Parameter.empty:
|
|
213
|
+
if param.annotation == int:
|
|
214
|
+
param_type = "integer"
|
|
215
|
+
elif param.annotation == float:
|
|
216
|
+
param_type = "number"
|
|
217
|
+
elif param.annotation == bool:
|
|
218
|
+
param_type = "boolean"
|
|
219
|
+
elif param.annotation == list:
|
|
220
|
+
param_type = "array"
|
|
221
|
+
|
|
222
|
+
params[param_name] = {
|
|
223
|
+
"type": param_type,
|
|
224
|
+
"description": param_name,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"name": tool.__name__,
|
|
229
|
+
"description": tool.__doc__ or "No description",
|
|
230
|
+
"parameters": {
|
|
231
|
+
"type": "object",
|
|
232
|
+
"properties": params,
|
|
233
|
+
"required": [
|
|
234
|
+
name for name, param in sig.parameters.items()
|
|
235
|
+
if param.default == inspect.Parameter.empty
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Error extracting tool schema: {e}")
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
schema = convert_function_to_openai_function_schema(tool)
|
|
245
|
+
return schema
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"Error converting tool to schema: {e}")
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def generate_tool_schemas(tools: List[Callable]) -> List[Dict[str, Any]]:
|
|
252
|
+
"""Generate OpenAI function schemas for a list of tools.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
tools: List of tool functions
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
List of OpenAI function schema dictionaries
|
|
259
|
+
"""
|
|
260
|
+
schemas = []
|
|
261
|
+
|
|
262
|
+
for tool in tools:
|
|
263
|
+
schema = extract_tool_schema(tool)
|
|
264
|
+
if schema:
|
|
265
|
+
schemas.append(schema)
|
|
266
|
+
|
|
267
|
+
return schemas
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def discover_and_register_tools(
|
|
271
|
+
source: Any,
|
|
272
|
+
registry: Optional[ToolRegistry] = None,
|
|
273
|
+
) -> List[str]:
|
|
274
|
+
"""Discover tools from a source and register them.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
source: Module, object, or list of tools to discover from
|
|
278
|
+
registry: Optional tool registry (uses global if None)
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
List of registered tool names
|
|
282
|
+
"""
|
|
283
|
+
if registry is None:
|
|
284
|
+
registry = get_global_registry()
|
|
285
|
+
|
|
286
|
+
tools = []
|
|
287
|
+
|
|
288
|
+
# Handle different source types
|
|
289
|
+
if inspect.ismodule(source):
|
|
290
|
+
tools = discover_tools_in_module(source)
|
|
291
|
+
elif isinstance(source, list):
|
|
292
|
+
tools = [t for t in source if callable(t)]
|
|
293
|
+
elif hasattr(source, "__dict__"):
|
|
294
|
+
tools = discover_tools_in_object(source)
|
|
295
|
+
elif callable(source):
|
|
296
|
+
tools = [source]
|
|
297
|
+
|
|
298
|
+
registered = []
|
|
299
|
+
for tool in tools:
|
|
300
|
+
try:
|
|
301
|
+
name = registry.register_tool(tool)
|
|
302
|
+
registered.append(name)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(f"Error registering tool {tool}: {e}")
|
|
305
|
+
|
|
306
|
+
logger.info(f"Registered {len(registered)} tool(s)")
|
|
307
|
+
return registered
|