crewplus 0.1.0__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: crewplus
3
- Version: 0.1.0
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
@@ -74,18 +74,17 @@ print(response)
74
74
  The `crewplus-base` repository is organized to separate core logic, tests, and documentation.
75
75
 
76
76
  ```
77
- crewplus-base/ # GitHub repo 名称
77
+ crewplus-base/ # GitHub repo name
78
78
  ├── pyproject.toml
79
79
  ├── README.md
80
80
  ├── LICENSE
81
81
  ├── CHANGELOG.md
82
- ├── crewplus/ # PyPI包名对应的目录
82
+ ├── crewplus/ # PyPI package name
83
83
  │ └── __init__.py
84
84
  │ └── services/
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
  │ └── ...
@@ -114,4 +113,4 @@ python -m twine upload --repository testpypi dist/*
114
113
  pip install -i https://test.pypi.org/simple/ crewplus
115
114
 
116
115
  # Deploy to official PyPI
117
- python -m twine upload dist/*
116
+ python -m twine upload dist/*
@@ -58,18 +58,17 @@ print(response)
58
58
  The `crewplus-base` repository is organized to separate core logic, tests, and documentation.
59
59
 
60
60
  ```
61
- crewplus-base/ # GitHub repo 名称
61
+ crewplus-base/ # GitHub repo name
62
62
  ├── pyproject.toml
63
63
  ├── README.md
64
64
  ├── LICENSE
65
65
  ├── CHANGELOG.md
66
- ├── crewplus/ # PyPI包名对应的目录
66
+ ├── crewplus/ # PyPI package name
67
67
  │ └── __init__.py
68
68
  │ └── services/
69
69
  │ └── __init__.py
70
70
  │ └── gemini_chat_model.py
71
71
  │ └── model_load_balancer.py
72
- │ └── vdb_service.py
73
72
  │ └── ...
74
73
  │ └── vectorstores/
75
74
  │ └── ...
@@ -98,4 +97,4 @@ python -m twine upload --repository testpypi dist/*
98
97
  pip install -i https://test.pypi.org/simple/ crewplus
99
98
 
100
99
  # Deploy to official PyPI
101
- python -m twine upload dist/*
100
+ python -m twine upload dist/*
@@ -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
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "crewplus"
9
- version = "0.1.0"
9
+ version = "0.1.2"
10
10
  description = "Base services for CrewPlus AI applications"
11
11
  authors = [
12
12
  { name = "Tim Liu", email = "tim@opsmateai.com" },
File without changes
File without changes