crewplus 0.1.1__tar.gz → 0.1.2__tar.gz
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 crewplus might be problematic. Click here for more details.
- {crewplus-0.1.1 → crewplus-0.1.2}/PKG-INFO +1 -2
- {crewplus-0.1.1 → crewplus-0.1.2}/README.md +0 -1
- crewplus-0.1.2/crewplus/services/model_load_balancer.py +184 -0
- {crewplus-0.1.1 → crewplus-0.1.2}/pyproject.toml +1 -1
- {crewplus-0.1.1 → crewplus-0.1.2}/LICENSE +0 -0
- {crewplus-0.1.1 → crewplus-0.1.2}/crewplus/__init__.py +0 -0
- {crewplus-0.1.1 → crewplus-0.1.2}/crewplus/services/__init__.py +0 -0
- {crewplus-0.1.1 → crewplus-0.1.2}/crewplus/services/gemini_chat_model.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: crewplus
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Base services for CrewPlus AI applications
|
|
5
5
|
Author-Email: Tim Liu <tim@opsmateai.com>
|
|
6
6
|
License: MIT
|
|
@@ -85,7 +85,6 @@ crewplus-base/ # GitHub repo name
|
|
|
85
85
|
│ └── __init__.py
|
|
86
86
|
│ └── gemini_chat_model.py
|
|
87
87
|
│ └── model_load_balancer.py
|
|
88
|
-
│ └── vdb_service.py
|
|
89
88
|
│ └── ...
|
|
90
89
|
│ └── vectorstores/
|
|
91
90
|
│ └── ...
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import random
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, List, Optional, Union
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from langchain_openai import AzureChatOpenAI, ChatOpenAI, AzureOpenAIEmbeddings
|
|
7
|
+
from .gemini_chat_model import GeminiChatModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModelLoadBalancer:
|
|
11
|
+
def __init__(self,
|
|
12
|
+
config_path: Optional[str] = "config/models_config.json",
|
|
13
|
+
config_data: Optional[Dict] = None,
|
|
14
|
+
logger: Optional[logging.Logger] = None):
|
|
15
|
+
"""
|
|
16
|
+
Initializes the ModelLoadBalancer.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
config_path: Path to the JSON configuration file.
|
|
20
|
+
config_data: A dictionary containing the model configuration.
|
|
21
|
+
logger: An optional logger instance. If not provided, a default one is created.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ValueError: If neither config_path nor config_data is provided.
|
|
25
|
+
"""
|
|
26
|
+
if not config_path and not config_data:
|
|
27
|
+
raise ValueError("Either 'config_path' or 'config_data' must be provided.")
|
|
28
|
+
|
|
29
|
+
self.config_path = config_path
|
|
30
|
+
self.config_data = config_data
|
|
31
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
32
|
+
self.models_config: List[Dict] = []
|
|
33
|
+
self.models: Dict[int, Union[AzureChatOpenAI, ChatOpenAI, AzureOpenAIEmbeddings, GeminiChatModel]] = {}
|
|
34
|
+
self._initialize_state()
|
|
35
|
+
self._config_loaded = False # Flag to check if config is loaded
|
|
36
|
+
|
|
37
|
+
def load_config(self):
|
|
38
|
+
"""Load and validate model configurations from a file path or a dictionary."""
|
|
39
|
+
self.logger.debug("Model balancer: loading configuration.")
|
|
40
|
+
try:
|
|
41
|
+
config = None
|
|
42
|
+
if self.config_data:
|
|
43
|
+
config = self.config_data
|
|
44
|
+
elif self.config_path:
|
|
45
|
+
with open(self.config_path, 'r') as f:
|
|
46
|
+
config = json.load(f)
|
|
47
|
+
else:
|
|
48
|
+
# This case is handled in __init__, but as a safeguard:
|
|
49
|
+
raise RuntimeError("No configuration source provided (path or data).")
|
|
50
|
+
|
|
51
|
+
# Validate config
|
|
52
|
+
if 'models' not in config or not isinstance(config['models'], list):
|
|
53
|
+
raise ValueError("Configuration must contain a 'models' list.")
|
|
54
|
+
|
|
55
|
+
for model in config.get('models', []):
|
|
56
|
+
if 'provider' not in model or 'type' not in model or 'id' not in model:
|
|
57
|
+
self.logger.error("Model config must contain 'id', 'provider', and 'type' fields.")
|
|
58
|
+
raise ValueError("Model config must contain 'id', 'provider', and 'type' fields.")
|
|
59
|
+
|
|
60
|
+
self.models_config = config['models']
|
|
61
|
+
|
|
62
|
+
# Instantiate models
|
|
63
|
+
for model_config in self.models_config:
|
|
64
|
+
model_id = model_config['id']
|
|
65
|
+
self.models[model_id] = self._instantiate_model(model_config)
|
|
66
|
+
|
|
67
|
+
self._config_loaded = True
|
|
68
|
+
self.logger.debug("Model balancer: configuration loaded successfully.")
|
|
69
|
+
except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
|
|
70
|
+
self._config_loaded = False
|
|
71
|
+
self.logger.error(f"Failed to load model configuration: {e}", exc_info=True)
|
|
72
|
+
raise RuntimeError(f"Failed to load model configuration: {e}")
|
|
73
|
+
|
|
74
|
+
def get_model(self, provider: str = None, model_type: str = None, deployment_name: str = None):
|
|
75
|
+
"""
|
|
76
|
+
Get a model instance.
|
|
77
|
+
|
|
78
|
+
Can fetch a model in two ways:
|
|
79
|
+
1. By its specific `deployment_name`.
|
|
80
|
+
2. By `provider` and `model_type`, which will select a model using round-robin.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
provider: The model provider (e.g., 'azure-openai', 'google-genai').
|
|
84
|
+
model_type: The type of model (e.g., 'inference', 'embedding').
|
|
85
|
+
deployment_name: The unique name for the model deployment.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
An instantiated language model object.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
RuntimeError: If the model configuration has not been loaded.
|
|
92
|
+
ValueError: If the requested model cannot be found or if parameters are insufficient.
|
|
93
|
+
"""
|
|
94
|
+
if not self._config_loaded:
|
|
95
|
+
self.logger.error("Model configuration not loaded")
|
|
96
|
+
raise RuntimeError("Model configuration not loaded")
|
|
97
|
+
|
|
98
|
+
if deployment_name:
|
|
99
|
+
for model_config in self.models_config:
|
|
100
|
+
if model_config.get('deployment_name') == deployment_name:
|
|
101
|
+
model_id = model_config['id']
|
|
102
|
+
return self.models[model_id]
|
|
103
|
+
self.logger.error(f"No model found for deployment name: {deployment_name}")
|
|
104
|
+
raise ValueError(f"No model found for deployment name: {deployment_name}")
|
|
105
|
+
|
|
106
|
+
if provider and model_type:
|
|
107
|
+
candidates = [model for model in self.models_config if model.get('provider') == provider and model.get('type') == model_type]
|
|
108
|
+
if not candidates:
|
|
109
|
+
self.logger.error(f"No models found for provider '{provider}' and type '{model_type}'")
|
|
110
|
+
raise ValueError(f"No models found for provider '{provider}' and type '{model_type}'")
|
|
111
|
+
|
|
112
|
+
selected_model_config = self._round_robin_selection(candidates)
|
|
113
|
+
model_id = selected_model_config['id']
|
|
114
|
+
return self.models[model_id]
|
|
115
|
+
|
|
116
|
+
raise ValueError("Either 'deployment_name' or both 'provider' and 'model_type' must be provided.")
|
|
117
|
+
|
|
118
|
+
def _instantiate_model(self, model_config: Dict):
|
|
119
|
+
"""Instantiate and return an LLM object based on the model configuration"""
|
|
120
|
+
provider = model_config['provider']
|
|
121
|
+
self.logger.debug(f"Model balancer: instantiating {provider} -- {model_config.get('deployment_name')}")
|
|
122
|
+
|
|
123
|
+
if provider == 'azure-openai':
|
|
124
|
+
kwargs = {
|
|
125
|
+
'azure_deployment': model_config['deployment_name'],
|
|
126
|
+
'openai_api_version': model_config['api_version'],
|
|
127
|
+
'azure_endpoint': model_config['api_base'],
|
|
128
|
+
'openai_api_key': model_config['api_key']
|
|
129
|
+
}
|
|
130
|
+
if 'temperature' in model_config:
|
|
131
|
+
kwargs['temperature'] = model_config['temperature']
|
|
132
|
+
if model_config.get('deployment_name') == 'o1-mini':
|
|
133
|
+
kwargs['disable_streaming'] = True
|
|
134
|
+
return AzureChatOpenAI(**kwargs)
|
|
135
|
+
elif provider == 'openai':
|
|
136
|
+
kwargs = {
|
|
137
|
+
'openai_api_key': model_config['api_key']
|
|
138
|
+
}
|
|
139
|
+
if 'temperature' in model_config:
|
|
140
|
+
kwargs['temperature'] = model_config['temperature']
|
|
141
|
+
return ChatOpenAI(**kwargs)
|
|
142
|
+
elif provider == 'azure-openai-embeddings':
|
|
143
|
+
return AzureOpenAIEmbeddings(
|
|
144
|
+
azure_deployment=model_config['deployment_name'],
|
|
145
|
+
openai_api_version=model_config['api_version'],
|
|
146
|
+
api_key=model_config['api_key'],
|
|
147
|
+
azure_endpoint=model_config['api_base'],
|
|
148
|
+
chunk_size=16, request_timeout=60, max_retries=2
|
|
149
|
+
)
|
|
150
|
+
elif provider == 'google-genai':
|
|
151
|
+
kwargs = {
|
|
152
|
+
'google_api_key': model_config['api_key'],
|
|
153
|
+
'model_name': model_config['deployment_name'] # Map deployment_name to model_name
|
|
154
|
+
}
|
|
155
|
+
if 'temperature' in model_config:
|
|
156
|
+
kwargs['temperature'] = model_config['temperature']
|
|
157
|
+
if 'max_tokens' in model_config:
|
|
158
|
+
kwargs['max_tokens'] = model_config['max_tokens']
|
|
159
|
+
return GeminiChatModel(**kwargs)
|
|
160
|
+
else:
|
|
161
|
+
self.logger.error(f"Unsupported provider: {provider}")
|
|
162
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
163
|
+
|
|
164
|
+
def _initialize_state(self):
|
|
165
|
+
self.active_models = []
|
|
166
|
+
self.usage_counter = defaultdict(int)
|
|
167
|
+
self.current_indices = {}
|
|
168
|
+
|
|
169
|
+
def _round_robin_selection(self, candidates: list) -> Dict:
|
|
170
|
+
if id(candidates) not in self.current_indices:
|
|
171
|
+
self.current_indices[id(candidates)] = 0
|
|
172
|
+
idx = self.current_indices[id(candidates)]
|
|
173
|
+
model = candidates[idx]
|
|
174
|
+
self.current_indices[id(candidates)] = (idx + 1) % len(candidates)
|
|
175
|
+
self.usage_counter[model['id']] += 1
|
|
176
|
+
|
|
177
|
+
return model
|
|
178
|
+
|
|
179
|
+
def _least_used_selection(self, candidates: list) -> Dict:
|
|
180
|
+
min_usage = min(self.usage_counter[m['model_id']] for m in candidates)
|
|
181
|
+
least_used = [m for m in candidates if self.usage_counter[m['model_id']] == min_usage]
|
|
182
|
+
model = random.choice(least_used)
|
|
183
|
+
self.usage_counter[model['id']] += 1
|
|
184
|
+
return model
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|