crewplus 0.3.1__tar.gz → 0.3.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.
- {crewplus-0.3.1 → crewplus-0.3.2}/PKG-INFO +2 -2
- crewplus-0.3.2/crewplus/__pycache__/__init__.cpython-311.pyc.1535298853824 +0 -0
- crewplus-0.3.2/crewplus/services/__pycache__/__init__.cpython-311.pyc.1535299230960 +0 -0
- crewplus-0.3.2/crewplus/services/__pycache__/gemini_chat_model.cpython-311.pyc.1535298773120 +0 -0
- crewplus-0.3.2/crewplus/services/__pycache__/runtime_model_factory.cpython-311.pyc.1959047557728 +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/gemini_chat_model.py +44 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/model_load_balancer.py +13 -0
- crewplus-0.3.2/crewplus/services/runtime_model_factory.py +400 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/pyproject.toml +2 -2
- crewplus-0.3.2/tests/output/drawing_ingestion/gpt-5.4_run1.md +88 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/gpt-5.4_run2.md +93 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/gpt-5.6-luna_run1.md +59 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/gpt-5.6-luna_run2.md +46 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/gpt-5.6-terra_run1.md +48 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/gpt-5.6-terra_run2.md +55 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/page24_150dpi.png +0 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/report.md +63 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/results.json +605 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session1-luna-terra/gpt-5.6-luna_run1.md +79 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session1-luna-terra/gpt-5.6-luna_run2.md +60 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session1-luna-terra/gpt-5.6-terra_run1.md +47 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session1-luna-terra/gpt-5.6-terra_run2.md +46 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session1-luna-terra/report.md +58 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session1-luna-terra/results.json +427 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session2-55-sol/gpt-5.5_run1.md +54 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session2-55-sol/gpt-5.5_run2.md +43 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session2-55-sol/gpt-5.6-sol_run1.md +45 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session2-55-sol/gpt-5.6-sol_run2.md +56 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session2-55-sol/page24_150dpi.png +0 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session2-55-sol/report.md +58 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session2-55-sol/results.json +421 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/session2-55-sol/summary-report-55-sol.md +346 -0
- crewplus-0.3.2/tests/output/drawing_ingestion/summary-report.md +287 -0
- crewplus-0.3.2/tests/test_pdf_drawing_ingestion_gpt55_sol.py +106 -0
- crewplus-0.3.2/tests/test_pdf_drawing_ingestion_gpt56.py +397 -0
- crewplus-0.3.2/tests/test_runtime_model_factory.py +79 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/LICENSE +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/README.md +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/__init__.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/callbacks/__init__.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/callbacks/async_langfuse_handler.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/__init__.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/azure_chat_model.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/capabilities/__init__.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/capabilities/base.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/capabilities/bash.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/capabilities/registry.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/capabilities/web_search.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/claude_chat_model.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/feedback.md +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/feedback_manager.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/init_services.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/oai_claude_chat_model.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/schemas/feedback.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/services/tracing_manager.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/utils/__init__.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/utils/schema_action.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/utils/schema_document_updater.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/utils/tracing_util.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/vectorstores/milvus/__init__.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/vectorstores/milvus/milvus_schema_manager.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/vectorstores/milvus/schema_milvus.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/crewplus/vectorstores/milvus/vdb_service.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/docs/GeminiChatModel.md +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/docs/ModelLoadBalancer.md +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/docs/VDBService.md +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/docs/index.md +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/README.md +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/conftest.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/test_bash_tools.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/test_claude_api.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/test_claude_custom_endpoint_integration.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/test_claude_langchain_openai.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/test_claude_taskflow_schema.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/test_custom_claude_endpoint.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/test_gemini3_pro_preview.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/test_gemini_bind_tools.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/test_oai_claude_chat_model.py +0 -0
- {crewplus-0.3.1 → crewplus-0.3.2}/tests/test_ops_models_streaming.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: crewplus
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Base services for CrewPlus AI applications
|
|
5
5
|
Author-Email: Tim Liu <tim@opsmateai.com>
|
|
6
6
|
License: MIT
|
|
@@ -11,7 +11,7 @@ Project-URL: Issues, https://github.com/your-org/crewplus-base/issues
|
|
|
11
11
|
Requires-Python: <4.0,>=3.11
|
|
12
12
|
Requires-Dist: langchain<1.0.0,>=0.3.25
|
|
13
13
|
Requires-Dist: langchain-openai>=0.3.24
|
|
14
|
-
Requires-Dist: google-genai>=1.
|
|
14
|
+
Requires-Dist: google-genai>=1.51.0
|
|
15
15
|
Requires-Dist: anthropic[vertex]>=0.76.0
|
|
16
16
|
Requires-Dist: langchain-milvus<0.3.0,>=0.2.1
|
|
17
17
|
Requires-Dist: langfuse<4.0.0,>=3.1.3
|
|
Binary file
|
|
Binary file
|
crewplus-0.3.2/crewplus/services/__pycache__/runtime_model_factory.cpython-311.pyc.1959047557728
ADDED
|
Binary file
|
|
@@ -257,6 +257,17 @@ class GeminiChatModel(BaseChatModel):
|
|
|
257
257
|
max_tokens: Optional[int] = Field(default=None, description="Maximum tokens to generate")
|
|
258
258
|
top_p: Optional[float] = Field(default=None, description="Top-p sampling parameter")
|
|
259
259
|
top_k: Optional[int] = Field(default=None, description="Top-k sampling parameter")
|
|
260
|
+
|
|
261
|
+
# Thinking configuration. All optional: when none of the three is set, no
|
|
262
|
+
# thinking_config is sent at all and the model's own default applies.
|
|
263
|
+
# Gemini 3.x takes thinking_level, Gemini 2.5 takes thinking_budget; sending
|
|
264
|
+
# both is rejected by the API, so _build_thinking_config picks one by model family.
|
|
265
|
+
thinking_level: Optional[str] = Field(
|
|
266
|
+
default=None, description="MINIMAL/LOW/MEDIUM/HIGH, Gemini 3.x only")
|
|
267
|
+
thinking_budget: Optional[int] = Field(
|
|
268
|
+
default=None, description="Thinking token budget, Gemini 2.5 only")
|
|
269
|
+
include_thoughts: Optional[bool] = Field(
|
|
270
|
+
default=None, description="Whether thought parts are returned in the response")
|
|
260
271
|
|
|
261
272
|
# Vertex AI specific configuration
|
|
262
273
|
use_vertex_ai: bool = Field(default=False, description="Use Vertex AI instead of Google AI Platform")
|
|
@@ -804,6 +815,33 @@ class GeminiChatModel(BaseChatModel):
|
|
|
804
815
|
except Exception as e:
|
|
805
816
|
self.logger.error(f"Failed to process message part at index {i}: {part_spec}. Error: {e}", exc_info=True)
|
|
806
817
|
|
|
818
|
+
def _build_thinking_config(self) -> Optional[types.ThinkingConfig]:
|
|
819
|
+
"""Builds the thinking config for the current model family.
|
|
820
|
+
|
|
821
|
+
Gemini 3.x accepts ``thinking_level`` (MINIMAL/LOW/MEDIUM/HIGH), Gemini 2.5
|
|
822
|
+
accepts ``thinking_budget`` (an integer). Passing both is rejected by the
|
|
823
|
+
service, so the parameter for the other family is ignored rather than being
|
|
824
|
+
an error -- that is what lets a single model config entry serve both
|
|
825
|
+
generations, so switching models only means changing deployment_name.
|
|
826
|
+
|
|
827
|
+
Returns None when nothing is configured, in which case the caller omits
|
|
828
|
+
thinking_config entirely and the model's default behaviour applies.
|
|
829
|
+
"""
|
|
830
|
+
is_gemini3 = self.model_name.startswith("gemini-3")
|
|
831
|
+
kwargs: Dict[str, Any] = {}
|
|
832
|
+
|
|
833
|
+
value = self.thinking_level if is_gemini3 else self.thinking_budget
|
|
834
|
+
if value is not None:
|
|
835
|
+
if is_gemini3:
|
|
836
|
+
kwargs["thinking_level"] = str(value).upper()
|
|
837
|
+
else:
|
|
838
|
+
kwargs["thinking_budget"] = int(value)
|
|
839
|
+
|
|
840
|
+
if self.include_thoughts is not None:
|
|
841
|
+
kwargs["include_thoughts"] = self.include_thoughts
|
|
842
|
+
|
|
843
|
+
return types.ThinkingConfig(**kwargs) if kwargs else None
|
|
844
|
+
|
|
807
845
|
def _prepare_generation_config(
|
|
808
846
|
self,
|
|
809
847
|
messages: List[BaseMessage],
|
|
@@ -826,6 +864,12 @@ class GeminiChatModel(BaseChatModel):
|
|
|
826
864
|
if stop:
|
|
827
865
|
config_params["stop_sequences"] = stop
|
|
828
866
|
|
|
867
|
+
# Omit thinking_config entirely when nothing is configured, so the model
|
|
868
|
+
# default applies instead of us pinning a tier.
|
|
869
|
+
thinking_config = self._build_thinking_config()
|
|
870
|
+
if thinking_config is not None:
|
|
871
|
+
config_params["thinking_config"] = thinking_config
|
|
872
|
+
|
|
829
873
|
# Handle system instructions
|
|
830
874
|
system_prompts = [msg.content for msg in messages if isinstance(msg, SystemMessage) and msg.content]
|
|
831
875
|
if system_prompts:
|
|
@@ -57,6 +57,13 @@ class ModelLoadBalancer:
|
|
|
57
57
|
"code_execution": None,
|
|
58
58
|
"url_context": None,
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
# Gemini thinking parameters. Passed through only when present in the model
|
|
62
|
+
# config, so an entry that omits them leaves the model fields at None and no
|
|
63
|
+
# thinking_config reaches the API. GeminiChatModel picks thinking_level for
|
|
64
|
+
# Gemini 3.x and thinking_budget for Gemini 2.5, ignoring the other one.
|
|
65
|
+
GEMINI_THINKING_KEYS = ("thinking_level", "thinking_budget", "include_thoughts")
|
|
66
|
+
|
|
60
67
|
def __init__(self,
|
|
61
68
|
config_path: Optional[str] = "config/models_config.json",
|
|
62
69
|
config_data: Optional[Dict] = None,
|
|
@@ -267,6 +274,9 @@ class ModelLoadBalancer:
|
|
|
267
274
|
kwargs['temperature'] = model_config['temperature']
|
|
268
275
|
if 'max_tokens' in model_config:
|
|
269
276
|
kwargs['max_tokens'] = model_config['max_tokens']
|
|
277
|
+
for key in self.GEMINI_THINKING_KEYS:
|
|
278
|
+
if key in model_config:
|
|
279
|
+
kwargs[key] = model_config[key]
|
|
270
280
|
if disable_streaming:
|
|
271
281
|
kwargs['disable_streaming'] = True
|
|
272
282
|
return GeminiChatModel(**kwargs)
|
|
@@ -334,6 +344,9 @@ class ModelLoadBalancer:
|
|
|
334
344
|
kwargs['temperature'] = model_config['temperature']
|
|
335
345
|
if 'max_tokens' in model_config:
|
|
336
346
|
kwargs['max_tokens'] = model_config['max_tokens']
|
|
347
|
+
for key in self.GEMINI_THINKING_KEYS:
|
|
348
|
+
if key in model_config:
|
|
349
|
+
kwargs[key] = model_config[key]
|
|
337
350
|
if disable_streaming:
|
|
338
351
|
kwargs['disable_streaming'] = True
|
|
339
352
|
return GeminiChatModel(**kwargs)
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from langchain_openai import AzureOpenAIEmbeddings, ChatOpenAI, OpenAIEmbeddings
|
|
9
|
+
|
|
10
|
+
from .azure_chat_model import TracedAzureChatOpenAI
|
|
11
|
+
from .claude_chat_model import ClaudeChatModel
|
|
12
|
+
from .gemini_chat_model import GeminiChatModel
|
|
13
|
+
from .oai_claude_chat_model import OAIClaudeChatModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RuntimeModelConfigError(ValueError):
|
|
17
|
+
"""Raised when a runtime model policy is missing required fields."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _first_present(data: Dict[str, Any], *keys: str) -> Any:
|
|
21
|
+
for key in keys:
|
|
22
|
+
value = data.get(key)
|
|
23
|
+
if value is not None and value != "":
|
|
24
|
+
return value
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _credentials(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
29
|
+
creds = config.get("credentials") or {}
|
|
30
|
+
if not isinstance(creds, dict):
|
|
31
|
+
raise RuntimeModelConfigError("model credentials must be a dictionary")
|
|
32
|
+
return creds
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _api_key(config: Dict[str, Any]) -> str:
|
|
36
|
+
creds = _credentials(config)
|
|
37
|
+
value = _first_present(config, "api_key", "apiKey") or _first_present(creds, "api_key", "apiKey")
|
|
38
|
+
if not value:
|
|
39
|
+
raise RuntimeModelConfigError("model apiKey is required")
|
|
40
|
+
return str(value)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _provider_type(config: Dict[str, Any]) -> str:
|
|
44
|
+
provider = _first_present(config, "providerType", "provider_type", "provider")
|
|
45
|
+
if not provider:
|
|
46
|
+
raise RuntimeModelConfigError("model providerType is required")
|
|
47
|
+
return str(provider).strip().lower().replace("-", "_")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _model_id(config: Dict[str, Any]) -> str:
|
|
51
|
+
value = _first_present(config, "modelId", "model_id", "model", "modelName", "model_name")
|
|
52
|
+
if not value:
|
|
53
|
+
raise RuntimeModelConfigError("modelId is required")
|
|
54
|
+
return str(value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _deployment_name(config: Dict[str, Any]) -> str:
|
|
58
|
+
return str(_first_present(config, "deploymentName", "deployment_name", "deployment") or _model_id(config))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _base_url(config: Dict[str, Any]) -> Optional[str]:
|
|
62
|
+
value = _first_present(config, "baseUrl", "base_url", "apiBase", "api_base", "azureEndpoint", "azure_endpoint")
|
|
63
|
+
return str(value).rstrip("/") if value else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _api_version(config: Dict[str, Any]) -> Optional[str]:
|
|
67
|
+
value = _first_present(config, "apiVersion", "api_version", "openaiApiVersion", "openai_api_version")
|
|
68
|
+
return str(value) if value else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _dimensions(config: Dict[str, Any]) -> Optional[int]:
|
|
72
|
+
value = _first_present(config, "dimensions", "embeddingDimensions", "embedding_dims")
|
|
73
|
+
if value is None:
|
|
74
|
+
return None
|
|
75
|
+
return int(value)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _with_v1(base_url: str) -> str:
|
|
79
|
+
normalized = base_url.rstrip("/")
|
|
80
|
+
if normalized.endswith("/v1"):
|
|
81
|
+
return normalized
|
|
82
|
+
return f"{normalized}/v1"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def model_config_fingerprint(config: Dict[str, Any]) -> str:
|
|
86
|
+
"""Return a stable fingerprint that avoids storing secrets in cache keys."""
|
|
87
|
+
redacted = dict(config)
|
|
88
|
+
creds = _credentials(config) if isinstance(config.get("credentials"), dict) else {}
|
|
89
|
+
if creds:
|
|
90
|
+
secret_payload = json.dumps(creds, sort_keys=True, default=str)
|
|
91
|
+
redacted["credentials"] = {
|
|
92
|
+
"secret_hash": hashlib.sha256(secret_payload.encode("utf-8")).hexdigest()
|
|
93
|
+
}
|
|
94
|
+
payload = json.dumps(redacted, sort_keys=True, default=str)
|
|
95
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class RuntimeModelFactory:
|
|
99
|
+
"""
|
|
100
|
+
Creates model instances from runtime dictionaries without modifying the
|
|
101
|
+
process-wide ModelLoadBalancer or environment variables.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, logger: Optional[logging.Logger] = None):
|
|
105
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
106
|
+
self._cache: Dict[str, Any] = {}
|
|
107
|
+
|
|
108
|
+
def create_embedding_model(self, model_config: Dict[str, Any], *, cache_scope: str = "runtime"):
|
|
109
|
+
cache_key = self._cache_key(cache_scope, "embedding", model_config)
|
|
110
|
+
if cache_key not in self._cache:
|
|
111
|
+
self._cache[cache_key] = self._instantiate_embedding(model_config)
|
|
112
|
+
return self._cache[cache_key]
|
|
113
|
+
|
|
114
|
+
def create_vision_model(self, model_config: Dict[str, Any], *, cache_scope: str = "runtime"):
|
|
115
|
+
cache_key = self._cache_key(cache_scope, "vision", model_config)
|
|
116
|
+
if cache_key not in self._cache:
|
|
117
|
+
self._cache[cache_key] = self._instantiate_chat_model(model_config)
|
|
118
|
+
return self._cache[cache_key]
|
|
119
|
+
|
|
120
|
+
def create_chat_model(self, model_config: Dict[str, Any], *, cache_scope: str = "runtime"):
|
|
121
|
+
cache_key = self._cache_key(cache_scope, "chat", model_config)
|
|
122
|
+
if cache_key not in self._cache:
|
|
123
|
+
self._cache[cache_key] = self._instantiate_chat_model(model_config)
|
|
124
|
+
return self._cache[cache_key]
|
|
125
|
+
|
|
126
|
+
def _cache_key(self, scope: str, capability: str, model_config: Dict[str, Any]) -> str:
|
|
127
|
+
tenant = _first_present(model_config, "tenantId", "tenant_id") or scope
|
|
128
|
+
connection_id = _first_present(model_config, "connectionId", "connection_id") or "no-connection"
|
|
129
|
+
return f"{tenant}:{connection_id}:{capability}:{model_config_fingerprint(model_config)}"
|
|
130
|
+
|
|
131
|
+
def _instantiate_embedding(self, model_config: Dict[str, Any]):
|
|
132
|
+
provider = _provider_type(model_config)
|
|
133
|
+
model = _model_id(model_config)
|
|
134
|
+
api_key = _api_key(model_config)
|
|
135
|
+
dimensions = _dimensions(model_config)
|
|
136
|
+
|
|
137
|
+
if provider in {"azure_openai", "azure_openai_embeddings"}:
|
|
138
|
+
api_version = _api_version(model_config)
|
|
139
|
+
base_url = _base_url(model_config)
|
|
140
|
+
if not api_version:
|
|
141
|
+
raise RuntimeModelConfigError("apiVersion is required for Azure OpenAI embeddings")
|
|
142
|
+
if not base_url:
|
|
143
|
+
raise RuntimeModelConfigError("baseUrl is required for Azure OpenAI embeddings")
|
|
144
|
+
|
|
145
|
+
kwargs = {
|
|
146
|
+
"model": model,
|
|
147
|
+
"azure_deployment": _deployment_name(model_config),
|
|
148
|
+
"openai_api_version": api_version,
|
|
149
|
+
"api_key": api_key,
|
|
150
|
+
"azure_endpoint": base_url,
|
|
151
|
+
"chunk_size": int(model_config.get("chunk_size", 16)),
|
|
152
|
+
"request_timeout": int(model_config.get("request_timeout", 60)),
|
|
153
|
+
"max_retries": int(model_config.get("max_retries", 2)),
|
|
154
|
+
}
|
|
155
|
+
if dimensions is not None:
|
|
156
|
+
kwargs["dimensions"] = dimensions
|
|
157
|
+
return AzureOpenAIEmbeddings(**kwargs)
|
|
158
|
+
|
|
159
|
+
if provider == "openai":
|
|
160
|
+
kwargs = {"model": model, "api_key": api_key}
|
|
161
|
+
if dimensions is not None:
|
|
162
|
+
kwargs["dimensions"] = dimensions
|
|
163
|
+
return OpenAIEmbeddings(**kwargs)
|
|
164
|
+
|
|
165
|
+
if provider == "openai_compat":
|
|
166
|
+
base_url = _base_url(model_config)
|
|
167
|
+
if not base_url:
|
|
168
|
+
raise RuntimeModelConfigError("baseUrl is required for OpenAI-compatible embeddings")
|
|
169
|
+
kwargs = {"model": model, "api_key": api_key, "base_url": _with_v1(base_url)}
|
|
170
|
+
if dimensions is not None:
|
|
171
|
+
kwargs["dimensions"] = dimensions
|
|
172
|
+
return OpenAIEmbeddings(**kwargs)
|
|
173
|
+
|
|
174
|
+
raise RuntimeModelConfigError(f"Unsupported runtime embedding provider: {provider}")
|
|
175
|
+
|
|
176
|
+
def _instantiate_chat_model(self, model_config: Dict[str, Any]):
|
|
177
|
+
provider = _provider_type(model_config)
|
|
178
|
+
model = _model_id(model_config)
|
|
179
|
+
api_key = _api_key(model_config) if provider not in {"vertex_google", "vertex_anthropic", "vertex_ai"} else None
|
|
180
|
+
temperature = float(model_config.get("temperature", 0.0))
|
|
181
|
+
max_tokens = model_config.get("max_tokens")
|
|
182
|
+
disable_streaming = bool(model_config.get("disable_streaming", False))
|
|
183
|
+
|
|
184
|
+
if provider == "azure_openai":
|
|
185
|
+
api_version = _api_version(model_config)
|
|
186
|
+
base_url = _base_url(model_config)
|
|
187
|
+
if not api_version:
|
|
188
|
+
raise RuntimeModelConfigError("apiVersion is required for Azure OpenAI models")
|
|
189
|
+
if not base_url:
|
|
190
|
+
raise RuntimeModelConfigError("baseUrl is required for Azure OpenAI models")
|
|
191
|
+
kwargs = {
|
|
192
|
+
"azure_deployment": _deployment_name(model_config),
|
|
193
|
+
"openai_api_version": api_version,
|
|
194
|
+
"azure_endpoint": base_url,
|
|
195
|
+
"openai_api_key": api_key,
|
|
196
|
+
"temperature": temperature,
|
|
197
|
+
}
|
|
198
|
+
if disable_streaming:
|
|
199
|
+
kwargs["disable_streaming"] = True
|
|
200
|
+
return TracedAzureChatOpenAI(**kwargs)
|
|
201
|
+
|
|
202
|
+
if provider == "openai":
|
|
203
|
+
kwargs = {"model": model, "openai_api_key": api_key, "temperature": temperature}
|
|
204
|
+
if max_tokens is not None:
|
|
205
|
+
kwargs["max_tokens"] = int(max_tokens)
|
|
206
|
+
if disable_streaming:
|
|
207
|
+
kwargs["disable_streaming"] = True
|
|
208
|
+
return ChatOpenAI(**kwargs)
|
|
209
|
+
|
|
210
|
+
if provider == "openai_compat":
|
|
211
|
+
base_url = _base_url(model_config)
|
|
212
|
+
if not base_url:
|
|
213
|
+
raise RuntimeModelConfigError("baseUrl is required for OpenAI-compatible models")
|
|
214
|
+
kwargs = {
|
|
215
|
+
"model": model,
|
|
216
|
+
"openai_api_key": api_key,
|
|
217
|
+
"base_url": _with_v1(base_url),
|
|
218
|
+
"temperature": temperature,
|
|
219
|
+
}
|
|
220
|
+
if max_tokens is not None:
|
|
221
|
+
kwargs["max_tokens"] = int(max_tokens)
|
|
222
|
+
if disable_streaming:
|
|
223
|
+
kwargs["disable_streaming"] = True
|
|
224
|
+
return ChatOpenAI(**kwargs)
|
|
225
|
+
|
|
226
|
+
if provider == "google":
|
|
227
|
+
kwargs = {
|
|
228
|
+
"google_api_key": api_key,
|
|
229
|
+
"model_name": _deployment_name(model_config),
|
|
230
|
+
"temperature": temperature,
|
|
231
|
+
}
|
|
232
|
+
if max_tokens is not None:
|
|
233
|
+
kwargs["max_tokens"] = int(max_tokens)
|
|
234
|
+
if disable_streaming:
|
|
235
|
+
kwargs["disable_streaming"] = True
|
|
236
|
+
return GeminiChatModel(**kwargs)
|
|
237
|
+
|
|
238
|
+
if provider in {"vertex_google", "vertex_ai"}:
|
|
239
|
+
project_id = _first_present(model_config, "projectId", "project_id", "gcpProjectId", "gcp_project_id")
|
|
240
|
+
location = _first_present(model_config, "location", "region", "gcpRegion", "gcp_region")
|
|
241
|
+
if not project_id or not location:
|
|
242
|
+
raise RuntimeModelConfigError("projectId and location are required for Vertex AI models")
|
|
243
|
+
kwargs = {
|
|
244
|
+
"use_vertex_ai": True,
|
|
245
|
+
"model_name": _deployment_name(model_config).split("@")[0],
|
|
246
|
+
"project_id": str(project_id),
|
|
247
|
+
"location": str(location),
|
|
248
|
+
"temperature": temperature,
|
|
249
|
+
}
|
|
250
|
+
service_account_file = _first_present(model_config, "serviceAccountFile", "service_account_file")
|
|
251
|
+
if service_account_file:
|
|
252
|
+
kwargs["service_account_file"] = str(service_account_file)
|
|
253
|
+
if max_tokens is not None:
|
|
254
|
+
kwargs["max_tokens"] = int(max_tokens)
|
|
255
|
+
if disable_streaming:
|
|
256
|
+
kwargs["disable_streaming"] = True
|
|
257
|
+
return GeminiChatModel(**kwargs)
|
|
258
|
+
|
|
259
|
+
if provider == "vertex_anthropic":
|
|
260
|
+
project_id = _first_present(model_config, "projectId", "project_id", "gcpProjectId", "gcp_project_id")
|
|
261
|
+
location = _first_present(model_config, "location", "region", "gcpRegion", "gcp_region")
|
|
262
|
+
if not project_id or not location:
|
|
263
|
+
raise RuntimeModelConfigError("projectId and location are required for Vertex Anthropic models")
|
|
264
|
+
kwargs = {
|
|
265
|
+
"model_name": _deployment_name(model_config).split("@")[0],
|
|
266
|
+
"project_id": str(project_id),
|
|
267
|
+
"location": str(location),
|
|
268
|
+
"temperature": temperature,
|
|
269
|
+
}
|
|
270
|
+
service_account_file = _first_present(model_config, "serviceAccountFile", "service_account_file")
|
|
271
|
+
if service_account_file:
|
|
272
|
+
kwargs["service_account_file"] = str(service_account_file)
|
|
273
|
+
if max_tokens is not None:
|
|
274
|
+
kwargs["max_tokens"] = int(max_tokens)
|
|
275
|
+
if disable_streaming:
|
|
276
|
+
kwargs["disable_streaming"] = True
|
|
277
|
+
return ClaudeChatModel(**kwargs)
|
|
278
|
+
|
|
279
|
+
if provider == "anthropic_compat":
|
|
280
|
+
base_url = _base_url(model_config)
|
|
281
|
+
if not base_url:
|
|
282
|
+
raise RuntimeModelConfigError("baseUrl is required for Anthropic-compatible models")
|
|
283
|
+
kwargs = {
|
|
284
|
+
"model_name": _deployment_name(model_config),
|
|
285
|
+
"use_custom_endpoint": True,
|
|
286
|
+
"api_key": api_key,
|
|
287
|
+
"base_url": base_url,
|
|
288
|
+
"temperature": temperature,
|
|
289
|
+
}
|
|
290
|
+
if max_tokens is not None:
|
|
291
|
+
kwargs["max_tokens"] = int(max_tokens)
|
|
292
|
+
if disable_streaming:
|
|
293
|
+
kwargs["disable_streaming"] = True
|
|
294
|
+
return ClaudeChatModel(**kwargs)
|
|
295
|
+
|
|
296
|
+
if provider == "custom_openai":
|
|
297
|
+
base_url = _base_url(model_config)
|
|
298
|
+
if not base_url:
|
|
299
|
+
raise RuntimeModelConfigError("baseUrl is required for custom OpenAI-compatible Claude models")
|
|
300
|
+
kwargs = {
|
|
301
|
+
"model": _deployment_name(model_config),
|
|
302
|
+
"api_key": api_key,
|
|
303
|
+
"base_url": _with_v1(base_url),
|
|
304
|
+
"capabilities_config": model_config.get("capabilities", {}),
|
|
305
|
+
"temperature": temperature,
|
|
306
|
+
}
|
|
307
|
+
if max_tokens is not None:
|
|
308
|
+
kwargs["max_tokens"] = int(max_tokens)
|
|
309
|
+
if disable_streaming:
|
|
310
|
+
kwargs["streaming"] = False
|
|
311
|
+
return OAIClaudeChatModel(**kwargs)
|
|
312
|
+
|
|
313
|
+
raise RuntimeModelConfigError(f"Unsupported runtime chat provider: {provider}")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class RuntimeModelPool:
|
|
317
|
+
"""Selects runtime models from a capability-specific model pool."""
|
|
318
|
+
|
|
319
|
+
RETRYABLE_STATUS_MARKERS = ("429", "408", "500", "502", "503", "504", "timeout", "temporarily", "rate limit")
|
|
320
|
+
|
|
321
|
+
def __init__(
|
|
322
|
+
self,
|
|
323
|
+
factory: Optional[RuntimeModelFactory] = None,
|
|
324
|
+
logger: Optional[logging.Logger] = None,
|
|
325
|
+
cooldown_seconds: int = 60,
|
|
326
|
+
max_cooldown_seconds: int = 300,
|
|
327
|
+
):
|
|
328
|
+
self.factory = factory or RuntimeModelFactory(logger=logger)
|
|
329
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
330
|
+
self.cooldown_seconds = cooldown_seconds
|
|
331
|
+
self.max_cooldown_seconds = max_cooldown_seconds
|
|
332
|
+
self._cooldowns: Dict[str, float] = {}
|
|
333
|
+
self._failures: Dict[str, int] = {}
|
|
334
|
+
|
|
335
|
+
def select_model(self, policy: Dict[str, Any], capability: str) -> Dict[str, Any]:
|
|
336
|
+
candidates = self._active_candidates(policy, capability)
|
|
337
|
+
strategy = str((policy.get(capability) or {}).get("selectionStrategy") or policy.get("selectionStrategy") or "random")
|
|
338
|
+
if strategy == "weighted_random":
|
|
339
|
+
return self._weighted_random(candidates)
|
|
340
|
+
return random.choice(candidates)
|
|
341
|
+
|
|
342
|
+
def get_model(self, policy: Dict[str, Any], capability: str, *, cache_scope: str = "runtime") -> Tuple[Any, Dict[str, Any]]:
|
|
343
|
+
selected = self.select_model(policy, capability)
|
|
344
|
+
if capability == "embedding":
|
|
345
|
+
model = self.factory.create_embedding_model(selected, cache_scope=cache_scope)
|
|
346
|
+
elif capability == "vision":
|
|
347
|
+
model = self.factory.create_vision_model(selected, cache_scope=cache_scope)
|
|
348
|
+
else:
|
|
349
|
+
model = self.factory.create_chat_model(selected, cache_scope=cache_scope)
|
|
350
|
+
return model, selected
|
|
351
|
+
|
|
352
|
+
def report_success(self, model_key: str) -> None:
|
|
353
|
+
self._failures.pop(model_key, None)
|
|
354
|
+
|
|
355
|
+
def report_failure(self, model_key: str, error: Exception) -> None:
|
|
356
|
+
self._failures[model_key] = self._failures.get(model_key, 0) + 1
|
|
357
|
+
if self.is_retryable_error(error):
|
|
358
|
+
retry_after = self._retry_after_seconds(error)
|
|
359
|
+
cooldown = retry_after if retry_after is not None else self.cooldown_seconds
|
|
360
|
+
cooldown = min(cooldown, self.max_cooldown_seconds)
|
|
361
|
+
self._cooldowns[model_key] = time.time() + cooldown
|
|
362
|
+
|
|
363
|
+
def model_key(self, model_config: Dict[str, Any]) -> str:
|
|
364
|
+
connection_id = _first_present(model_config, "connectionId", "connection_id") or "no-connection"
|
|
365
|
+
return f"{connection_id}:{_provider_type(model_config)}:{_deployment_name(model_config)}:{model_config_fingerprint(model_config)}"
|
|
366
|
+
|
|
367
|
+
def is_retryable_error(self, error: Exception) -> bool:
|
|
368
|
+
text = str(error).lower()
|
|
369
|
+
return any(marker in text for marker in self.RETRYABLE_STATUS_MARKERS)
|
|
370
|
+
|
|
371
|
+
def _active_candidates(self, policy: Dict[str, Any], capability: str) -> List[Dict[str, Any]]:
|
|
372
|
+
section = policy.get(capability) or {}
|
|
373
|
+
models = section.get("models")
|
|
374
|
+
if not isinstance(models, list) or not models:
|
|
375
|
+
raise RuntimeModelConfigError(f"No runtime models configured for capability: {capability}")
|
|
376
|
+
|
|
377
|
+
now = time.time()
|
|
378
|
+
active = [model for model in models if self._cooldowns.get(self.model_key(model), 0) <= now]
|
|
379
|
+
return active or models
|
|
380
|
+
|
|
381
|
+
def _weighted_random(self, candidates: Iterable[Dict[str, Any]]) -> Dict[str, Any]:
|
|
382
|
+
items = list(candidates)
|
|
383
|
+
weights = [max(float(item.get("weight", 1) or 1), 0.0) for item in items]
|
|
384
|
+
if not any(weights):
|
|
385
|
+
return random.choice(items)
|
|
386
|
+
return random.choices(items, weights=weights, k=1)[0]
|
|
387
|
+
|
|
388
|
+
def _retry_after_seconds(self, error: Exception) -> Optional[int]:
|
|
389
|
+
retry_after = getattr(error, "retry_after", None)
|
|
390
|
+
if retry_after is None:
|
|
391
|
+
response = getattr(error, "response", None)
|
|
392
|
+
headers = getattr(response, "headers", None)
|
|
393
|
+
if headers:
|
|
394
|
+
retry_after = headers.get("Retry-After") or headers.get("retry-after")
|
|
395
|
+
if retry_after is None:
|
|
396
|
+
return None
|
|
397
|
+
try:
|
|
398
|
+
return int(float(retry_after))
|
|
399
|
+
except (TypeError, ValueError):
|
|
400
|
+
return None
|
|
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "crewplus"
|
|
9
|
-
version = "0.3.
|
|
9
|
+
version = "0.3.2"
|
|
10
10
|
description = "Base services for CrewPlus AI applications"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "Tim Liu", email = "tim@opsmateai.com" },
|
|
@@ -16,7 +16,7 @@ requires-python = ">=3.11,<4.0"
|
|
|
16
16
|
dependencies = [
|
|
17
17
|
"langchain>=0.3.25, <1.0.0",
|
|
18
18
|
"langchain-openai>=0.3.24",
|
|
19
|
-
"google-genai>=1.
|
|
19
|
+
"google-genai>=1.51.0",
|
|
20
20
|
"anthropic[vertex]>=0.76.0",
|
|
21
21
|
"langchain-milvus (>=0.2.1,<0.3.0)",
|
|
22
22
|
"langfuse (>=3.1.3,<4.0.0)",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Technical drawing page content extraction
|
|
2
|
+
|
|
3
|
+
Main drawing
|
|
4
|
+
- Assembly drawing of a rotating shaft support arrangement.
|
|
5
|
+
- A long horizontal shaft is shown mounted between two pedestal-like supports on a base plate.
|
|
6
|
+
- Left side: a support with a small side block/retainer and several numbered callouts.
|
|
7
|
+
- Right side: a sectional view through the support and bearing arrangement, heavily hatched to indicate cut material.
|
|
8
|
+
- A centerline runs horizontally through the shaft.
|
|
9
|
+
- Section indicator text near the sectional view: A-A, h/rechts
|
|
10
|
+
- Visible dimensions:
|
|
11
|
+
- 150
|
|
12
|
+
- 51
|
|
13
|
+
|
|
14
|
+
Numbered callouts visible on drawing
|
|
15
|
+
- Left support area:
|
|
16
|
+
- 11
|
|
17
|
+
- 1
|
|
18
|
+
- 5
|
|
19
|
+
- 12
|
|
20
|
+
- 13
|
|
21
|
+
- Right sectional support area:
|
|
22
|
+
- 2
|
|
23
|
+
- 7
|
|
24
|
+
- 8
|
|
25
|
+
- 9
|
|
26
|
+
- 4
|
|
27
|
+
- 10
|
|
28
|
+
- 3
|
|
29
|
+
- Base area:
|
|
30
|
+
- 6
|
|
31
|
+
|
|
32
|
+
Bill of materials / parts list table
|
|
33
|
+
Columns visible:
|
|
34
|
+
- Pos.
|
|
35
|
+
- St.
|
|
36
|
+
- Benennung
|
|
37
|
+
- Norm / Maße / Werkstoff
|
|
38
|
+
- Zeichnungs-Nr.
|
|
39
|
+
|
|
40
|
+
Rows extracted as visible:
|
|
41
|
+
- 13 | 4 | Unterlegscheibe | HB 200 |
|
|
42
|
+
- 12 | 4 | Sechskantschr. MGS | 8.8, M20x30 |
|
|
43
|
+
- 11 | 4 | Flachrundschr. MGS | 8.8, M10x55 |
|
|
44
|
+
- 10 | 2 | Unterlegscheibe | HB 200 |
|
|
45
|
+
- 9 | 3 | Zylinderschr. MGS | M8x25 |
|
|
46
|
+
- 8 | 3 | Unterlegscheibe | HB 200 |
|
|
47
|
+
- 7 | 2 | Paßfeder | 18x11x90, PRK 6x6x61 |
|
|
48
|
+
- 6 | 1 | Platte | 1650-06 |
|
|
49
|
+
- 5 | 2 | Lagerbock | Stahl, ST37M |
|
|
50
|
+
- 4 | 3 | Büchse | | VKT-071004-B
|
|
51
|
+
- 3 | 2 | Radkranz | | VKT-071004-A/4
|
|
52
|
+
- 2 | 1 | Nabe | | VKT-071004-A/3
|
|
53
|
+
- 1 | 2 | Nabensicherung | | VKT-071004-A/2
|
|
54
|
+
|
|
55
|
+
Title block / metadata
|
|
56
|
+
- Maßstab: 1:1
|
|
57
|
+
- Format: A3
|
|
58
|
+
- Werkstoff / material field: St
|
|
59
|
+
- Benennung / title:
|
|
60
|
+
- RFL-Lin
|
|
61
|
+
- rotating shaft - Assembly
|
|
62
|
+
- Zeichnungs-Nr. / drawing number:
|
|
63
|
+
- VKT-071005-A
|
|
64
|
+
|
|
65
|
+
Revision / issue table
|
|
66
|
+
- Gez. / date fields visible but handwritten/typed entries are faint.
|
|
67
|
+
- Name visible: MKT
|
|
68
|
+
- Datum visible in one field: 04/18
|
|
69
|
+
- Aenderung durchgefuehrt / geprueft o.ä. field present at lower left title area.
|
|
70
|
+
- Freimaß / tolerance or standards field present but not fully legible.
|
|
71
|
+
|
|
72
|
+
Detailed image/diagram description
|
|
73
|
+
- The page is a monochrome engineering drawing sheet with a border and title block.
|
|
74
|
+
- The assembly consists of a cylindrical shaft supported by two tapered or trapezoidal pedestals fixed to a rectangular base plate.
|
|
75
|
+
- The left support is shown externally. A side-mounted fastening arrangement appears to include a flat-head or round-head screw, washer, and retaining/nabensicherung component. A pedestal labeled by callout 5 sits on the base plate 6. Above it is a wheel/ring-like component mounted on the shaft.
|
|
76
|
+
- The right support is shown in cross-section. The section reveals:
|
|
77
|
+
- A central shaft passing vertically through the cut assembly view.
|
|
78
|
+
- A hub/nabe around the shaft.
|
|
79
|
+
- A radial key/Paßfeder between shaft and hub.
|
|
80
|
+
- A large outer ring/radkranz surrounding the hub area.
|
|
81
|
+
- Bushings/büchsen and washers stacked near the top.
|
|
82
|
+
- Multiple screws entering from the top and side to secure the components.
|
|
83
|
+
- The hatched regions indicate the cut metallic parts of the support and surrounding structure.
|
|
84
|
+
- The shaft extends across the page between the two supports and appears aligned along a dashed centerline.
|
|
85
|
+
- The base plate is a long rectangular member beneath both supports, with one horizontal dimension of 150 shown between support-related reference points.
|
|
86
|
+
- The sectional support at right includes a top clamping/cover region with bolts and washers, and an internal fit around the shaft that suggests a keyed hub or wheel mounting.
|
|
87
|
+
- Number balloons point to nearly all major components, corresponding to the parts list.
|
|
88
|
+
- The drawing is rotated relative to normal reading orientation in the provided image; the title block and parts list are on the right side when viewed as provided.
|