refactored ai core system to attach ai models plug and play
This commit is contained in:
parent
52adedab4a
commit
3adaaad8eb
11 changed files with 2721 additions and 688 deletions
82
modules/aicore/aicoreBase.py
Normal file
82
modules/aicore/aicoreBase.py
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
"""
|
||||||
|
Base connector interface for AI connectors.
|
||||||
|
All AI connectors should inherit from this class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
from modules.datamodels.datamodelAi import AiModel
|
||||||
|
|
||||||
|
|
||||||
|
class BaseConnectorAi(ABC):
|
||||||
|
"""Base class for all AI connectors."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._models_cache: Optional[List[AiModel]] = None
|
||||||
|
self._last_cache_update: Optional[float] = None
|
||||||
|
self._cache_ttl: float = 300.0 # 5 minutes cache TTL
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getModels(self) -> List[AiModel]:
|
||||||
|
"""
|
||||||
|
Get all available models for this connector.
|
||||||
|
Should be implemented by each connector.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getConnectorType(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the connector type identifier.
|
||||||
|
Should return one of: openai, anthropic, perplexity, tavily
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getCachedModels(self) -> List[AiModel]:
|
||||||
|
"""
|
||||||
|
Get cached models with TTL check.
|
||||||
|
Returns cached models if still valid, otherwise refreshes cache.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Check if cache is valid
|
||||||
|
if (self._models_cache is not None and
|
||||||
|
self._last_cache_update is not None and
|
||||||
|
current_time - self._last_cache_update < self._cache_ttl):
|
||||||
|
return self._models_cache
|
||||||
|
|
||||||
|
# Refresh cache
|
||||||
|
self._models_cache = self.getModels()
|
||||||
|
self._last_cache_update = current_time
|
||||||
|
|
||||||
|
return self._models_cache
|
||||||
|
|
||||||
|
def clearCache(self):
|
||||||
|
"""Clear the models cache."""
|
||||||
|
self._models_cache = None
|
||||||
|
self._last_cache_update = None
|
||||||
|
|
||||||
|
def getModelByName(self, name: str) -> Optional[AiModel]:
|
||||||
|
"""Get a specific model by name."""
|
||||||
|
models = self.getCachedModels()
|
||||||
|
for model in models:
|
||||||
|
if model.name == name:
|
||||||
|
return model
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getModelsByCapability(self, capability: str) -> List[AiModel]:
|
||||||
|
"""Get models that support a specific capability."""
|
||||||
|
models = self.getCachedModels()
|
||||||
|
return [model for model in models if capability in model.capabilities]
|
||||||
|
|
||||||
|
def getModelsByTag(self, tag: str) -> List[AiModel]:
|
||||||
|
"""Get models that have a specific tag."""
|
||||||
|
models = self.getCachedModels()
|
||||||
|
return [model for model in models if tag in model.tags]
|
||||||
|
|
||||||
|
def getAvailableModels(self) -> List[AiModel]:
|
||||||
|
"""Get only available models."""
|
||||||
|
models = self.getCachedModels()
|
||||||
|
return [model for model in models if model.isAvailable]
|
||||||
175
modules/aicore/aicoreModelRegistry.py
Normal file
175
modules/aicore/aicoreModelRegistry.py
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
"""
|
||||||
|
Dynamic model registry that collects models from all AI connectors.
|
||||||
|
Implements plugin-like architecture for connector discovery.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
from modules.datamodels.datamodelAi import AiModel
|
||||||
|
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelRegistry:
|
||||||
|
"""Dynamic registry for AI models from all connectors."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._models: Dict[str, AiModel] = {}
|
||||||
|
self._connectors: Dict[str, BaseConnectorAi] = {}
|
||||||
|
self._last_refresh: Optional[float] = None
|
||||||
|
self._refresh_interval: float = 300.0 # 5 minutes
|
||||||
|
|
||||||
|
def registerConnector(self, connector: BaseConnectorAi):
|
||||||
|
"""Register a connector and collect its models."""
|
||||||
|
connector_type = connector.getConnectorType()
|
||||||
|
self._connectors[connector_type] = connector
|
||||||
|
|
||||||
|
# Collect models from this connector
|
||||||
|
try:
|
||||||
|
models = connector.getCachedModels()
|
||||||
|
for model in models:
|
||||||
|
self._models[model.name] = model
|
||||||
|
logger.debug(f"Registered model: {model.name} from {connector_type}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to register models from {connector_type}: {e}")
|
||||||
|
|
||||||
|
def discoverConnectors(self) -> List[BaseConnectorAi]:
|
||||||
|
"""Auto-discover connectors by scanning aicorePlugin*.py files."""
|
||||||
|
connectors = []
|
||||||
|
connector_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
# Scan for connector files
|
||||||
|
for filename in os.listdir(connector_dir):
|
||||||
|
if filename.startswith('aicorePlugin') and filename.endswith('.py'):
|
||||||
|
module_name = filename[:-3] # Remove .py extension
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import the module
|
||||||
|
module = importlib.import_module(f'modules.connectors.{module_name}')
|
||||||
|
|
||||||
|
# Find connector classes (classes that inherit from BaseConnectorAi)
|
||||||
|
for attr_name in dir(module):
|
||||||
|
attr = getattr(module, attr_name)
|
||||||
|
if (isinstance(attr, type) and
|
||||||
|
issubclass(attr, BaseConnectorAi) and
|
||||||
|
attr != BaseConnectorAi):
|
||||||
|
|
||||||
|
# Instantiate the connector
|
||||||
|
connector = attr()
|
||||||
|
connectors.append(connector)
|
||||||
|
logger.info(f"Discovered connector: {connector.getConnectorType()}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to discover connector from {filename}: {e}")
|
||||||
|
|
||||||
|
return connectors
|
||||||
|
|
||||||
|
def refreshModels(self, force: bool = False):
|
||||||
|
"""Refresh models from all registered connectors."""
|
||||||
|
import time
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Check if refresh is needed
|
||||||
|
if (not force and
|
||||||
|
self._last_refresh is not None and
|
||||||
|
current_time - self._last_refresh < self._refresh_interval):
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Refreshing model registry...")
|
||||||
|
|
||||||
|
# Clear existing models
|
||||||
|
self._models.clear()
|
||||||
|
|
||||||
|
# Re-register all connectors
|
||||||
|
for connector in self._connectors.values():
|
||||||
|
try:
|
||||||
|
connector.clearCache() # Clear connector cache
|
||||||
|
models = connector.getCachedModels()
|
||||||
|
for model in models:
|
||||||
|
self._models[model.name] = model
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to refresh models from {connector.getConnectorType()}: {e}")
|
||||||
|
|
||||||
|
self._last_refresh = current_time
|
||||||
|
logger.info(f"Model registry refreshed: {len(self._models)} models available")
|
||||||
|
|
||||||
|
def getModel(self, name: str) -> Optional[AiModel]:
|
||||||
|
"""Get a specific model by name."""
|
||||||
|
self.refreshModels()
|
||||||
|
return self._models.get(name)
|
||||||
|
|
||||||
|
def getModels(self) -> List[AiModel]:
|
||||||
|
"""Get all available models."""
|
||||||
|
self.refreshModels()
|
||||||
|
return list(self._models.values())
|
||||||
|
|
||||||
|
def getModelsByConnector(self, connector_type: str) -> List[AiModel]:
|
||||||
|
"""Get models from a specific connector."""
|
||||||
|
self.refreshModels()
|
||||||
|
return [model for model in self._models.values() if model.connectorType == connector_type]
|
||||||
|
|
||||||
|
def getModelsByCapability(self, capability: str) -> List[AiModel]:
|
||||||
|
"""Get models that support a specific capability."""
|
||||||
|
self.refreshModels()
|
||||||
|
return [model for model in self._models.values() if capability in model.capabilities]
|
||||||
|
|
||||||
|
def getModelsByTag(self, tag: str) -> List[AiModel]:
|
||||||
|
"""Get models that have a specific tag."""
|
||||||
|
self.refreshModels()
|
||||||
|
return [model for model in self._models.values() if tag in model.tags]
|
||||||
|
|
||||||
|
def getAvailableModels(self) -> List[AiModel]:
|
||||||
|
"""Get only available models."""
|
||||||
|
self.refreshModels()
|
||||||
|
return [model for model in self._models.values() if model.isAvailable]
|
||||||
|
|
||||||
|
def getConnectorForModel(self, model_name: str) -> Optional[BaseConnectorAi]:
|
||||||
|
"""Get the connector instance for a specific model."""
|
||||||
|
model = self.getModel(model_name)
|
||||||
|
if model:
|
||||||
|
return self._connectors.get(model.connectorType)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getModelStats(self) -> Dict[str, Any]:
|
||||||
|
"""Get statistics about the model registry."""
|
||||||
|
self.refreshModels()
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"total_models": len(self._models),
|
||||||
|
"available_models": len([m for m in self._models.values() if m.isAvailable]),
|
||||||
|
"connectors": len(self._connectors),
|
||||||
|
"by_connector": {},
|
||||||
|
"by_capability": {},
|
||||||
|
"by_tag": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Count by connector
|
||||||
|
for model in self._models.values():
|
||||||
|
connector = model.connectorType
|
||||||
|
if connector not in stats["by_connector"]:
|
||||||
|
stats["by_connector"][connector] = 0
|
||||||
|
stats["by_connector"][connector] += 1
|
||||||
|
|
||||||
|
# Count by capability
|
||||||
|
for model in self._models.values():
|
||||||
|
for capability in model.capabilities:
|
||||||
|
if capability not in stats["by_capability"]:
|
||||||
|
stats["by_capability"][capability] = 0
|
||||||
|
stats["by_capability"][capability] += 1
|
||||||
|
|
||||||
|
# Count by tag
|
||||||
|
for model in self._models.values():
|
||||||
|
for tag in model.tags:
|
||||||
|
if tag not in stats["by_tag"]:
|
||||||
|
stats["by_tag"][tag] = 0
|
||||||
|
stats["by_tag"][tag] += 1
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
# Global registry instance
|
||||||
|
model_registry = ModelRegistry()
|
||||||
180
modules/aicore/aicoreModelSelectionConfig.py
Normal file
180
modules/aicore/aicoreModelSelectionConfig.py
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
"""
|
||||||
|
Configuration for dynamic model selection rules.
|
||||||
|
This makes model selection configurable rather than hardcoded.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from modules.datamodels.datamodelAi import OperationType, Priority, ProcessingMode, ModelTags
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SelectionRule:
|
||||||
|
"""A rule for model selection."""
|
||||||
|
name: str
|
||||||
|
condition: str # Description of when this rule applies
|
||||||
|
weight: float # Weight for scoring (higher = more important)
|
||||||
|
operation_types: List[str] # Operation types this rule applies to
|
||||||
|
required_tags: List[str] # Required tags for this rule
|
||||||
|
preferred_tags: List[str] # Preferred tags for this rule
|
||||||
|
avoid_tags: List[str] # Tags to avoid for this rule
|
||||||
|
min_quality_rating: Optional[int] = None # Minimum quality rating
|
||||||
|
max_cost: Optional[float] = None # Maximum cost threshold
|
||||||
|
min_context_length: Optional[int] = None # Minimum context length required
|
||||||
|
|
||||||
|
|
||||||
|
class ModelSelectionConfig:
|
||||||
|
"""Configuration for model selection rules."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.rules = self._loadDefaultRules()
|
||||||
|
self.fallback_models = self._loadFallbackModels()
|
||||||
|
|
||||||
|
def _loadDefaultRules(self) -> List[SelectionRule]:
|
||||||
|
"""Load default selection rules."""
|
||||||
|
return [
|
||||||
|
# High quality for planning and analysis
|
||||||
|
SelectionRule(
|
||||||
|
name="high_quality_analysis",
|
||||||
|
condition="Planning or analysis operations requiring high quality",
|
||||||
|
weight=10.0,
|
||||||
|
operation_types=[OperationType.GENERATE_PLAN, OperationType.ANALYSE_CONTENT],
|
||||||
|
required_tags=[ModelTags.TEXT, ModelTags.REASONING, ModelTags.ANALYSIS],
|
||||||
|
preferred_tags=[ModelTags.HIGH_QUALITY],
|
||||||
|
avoid_tags=[ModelTags.FAST],
|
||||||
|
min_quality_rating=8
|
||||||
|
),
|
||||||
|
|
||||||
|
# Fast processing for basic operations
|
||||||
|
SelectionRule(
|
||||||
|
name="fast_basic_processing",
|
||||||
|
condition="Basic operations requiring speed",
|
||||||
|
weight=8.0,
|
||||||
|
operation_types=[OperationType.GENERAL],
|
||||||
|
required_tags=[ModelTags.TEXT, ModelTags.CHAT],
|
||||||
|
preferred_tags=[ModelTags.FAST],
|
||||||
|
avoid_tags=[],
|
||||||
|
min_quality_rating=5
|
||||||
|
),
|
||||||
|
|
||||||
|
# Cost-effective for high-volume operations
|
||||||
|
SelectionRule(
|
||||||
|
name="cost_effective_processing",
|
||||||
|
condition="High-volume operations where cost matters",
|
||||||
|
weight=7.0,
|
||||||
|
operation_types=[OperationType.GENERAL, OperationType.GENERATE_CONTENT],
|
||||||
|
required_tags=[ModelTags.TEXT],
|
||||||
|
preferred_tags=[ModelTags.COST_EFFECTIVE],
|
||||||
|
avoid_tags=[],
|
||||||
|
max_cost=0.01 # $0.01 per 1k tokens
|
||||||
|
),
|
||||||
|
|
||||||
|
# Image analysis specific
|
||||||
|
SelectionRule(
|
||||||
|
name="image_analysis",
|
||||||
|
condition="Image analysis operations",
|
||||||
|
weight=10.0,
|
||||||
|
operation_types=[OperationType.IMAGE_ANALYSIS],
|
||||||
|
required_tags=[ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL],
|
||||||
|
preferred_tags=[ModelTags.HIGH_QUALITY],
|
||||||
|
avoid_tags=[],
|
||||||
|
min_quality_rating=8
|
||||||
|
),
|
||||||
|
|
||||||
|
# Web research specific
|
||||||
|
SelectionRule(
|
||||||
|
name="web_research",
|
||||||
|
condition="Web research operations",
|
||||||
|
weight=9.0,
|
||||||
|
operation_types=[OperationType.WEB_RESEARCH],
|
||||||
|
required_tags=[ModelTags.TEXT, ModelTags.ANALYSIS],
|
||||||
|
preferred_tags=[ModelTags.WEB, ModelTags.SEARCH],
|
||||||
|
avoid_tags=[],
|
||||||
|
min_quality_rating=7
|
||||||
|
),
|
||||||
|
|
||||||
|
# Large context requirements
|
||||||
|
SelectionRule(
|
||||||
|
name="large_context",
|
||||||
|
condition="Operations requiring large context",
|
||||||
|
weight=8.0,
|
||||||
|
operation_types=[OperationType.GENERAL, OperationType.ANALYSE_CONTENT],
|
||||||
|
required_tags=[ModelTags.TEXT],
|
||||||
|
preferred_tags=[],
|
||||||
|
avoid_tags=[],
|
||||||
|
min_context_length=100000 # 100k tokens
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _loadFallbackModels(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""Load fallback model selection criteria."""
|
||||||
|
return {
|
||||||
|
OperationType.GENERAL: {
|
||||||
|
"priority_order": ["speed", "quality", "cost"],
|
||||||
|
"required_tags": [ModelTags.TEXT, ModelTags.CHAT],
|
||||||
|
"min_quality_rating": 5,
|
||||||
|
"max_cost_per_1k": 0.01
|
||||||
|
},
|
||||||
|
OperationType.IMAGE_ANALYSIS: {
|
||||||
|
"priority_order": ["quality", "speed"],
|
||||||
|
"required_tags": [ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL],
|
||||||
|
"min_quality_rating": 8,
|
||||||
|
"max_cost_per_1k": 0.1
|
||||||
|
},
|
||||||
|
OperationType.IMAGE_GENERATION: {
|
||||||
|
"priority_order": ["quality", "speed"],
|
||||||
|
"required_tags": [ModelTags.IMAGE_GENERATION, ModelTags.ART, ModelTags.VISUAL],
|
||||||
|
"min_quality_rating": 8,
|
||||||
|
"max_cost_per_1k": 0.1
|
||||||
|
},
|
||||||
|
OperationType.WEB_RESEARCH: {
|
||||||
|
"priority_order": ["quality", "speed", "cost"],
|
||||||
|
"required_tags": [ModelTags.TEXT, ModelTags.ANALYSIS],
|
||||||
|
"preferred_tags": [ModelTags.WEB, ModelTags.SEARCH],
|
||||||
|
"min_quality_rating": 7,
|
||||||
|
"max_cost_per_1k": 0.02
|
||||||
|
},
|
||||||
|
OperationType.GENERATE_PLAN: {
|
||||||
|
"priority_order": ["quality", "speed"],
|
||||||
|
"required_tags": [ModelTags.TEXT, ModelTags.REASONING, ModelTags.ANALYSIS],
|
||||||
|
"preferred_tags": [ModelTags.HIGH_QUALITY],
|
||||||
|
"min_quality_rating": 8,
|
||||||
|
"max_cost_per_1k": 0.1
|
||||||
|
},
|
||||||
|
OperationType.ANALYSE_CONTENT: {
|
||||||
|
"priority_order": ["quality", "speed"],
|
||||||
|
"required_tags": [ModelTags.TEXT, ModelTags.ANALYSIS, ModelTags.REASONING],
|
||||||
|
"preferred_tags": [ModelTags.HIGH_QUALITY],
|
||||||
|
"min_quality_rating": 8,
|
||||||
|
"max_cost_per_1k": 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getRulesForOperation(self, operation_type: str) -> List[SelectionRule]:
|
||||||
|
"""Get rules that apply to a specific operation type."""
|
||||||
|
return [rule for rule in self.rules if operation_type in rule.operation_types]
|
||||||
|
|
||||||
|
def getFallbackCriteria(self, operation_type: str) -> Dict[str, Any]:
|
||||||
|
"""Get fallback selection criteria for a specific operation type."""
|
||||||
|
return self.fallback_models.get(operation_type, self.fallback_models[OperationType.GENERAL])
|
||||||
|
|
||||||
|
def addRule(self, rule: SelectionRule):
|
||||||
|
"""Add a new selection rule."""
|
||||||
|
self.rules.append(rule)
|
||||||
|
|
||||||
|
def removeRule(self, rule_name: str):
|
||||||
|
"""Remove a selection rule by name."""
|
||||||
|
self.rules = [rule for rule in self.rules if rule.name != rule_name]
|
||||||
|
|
||||||
|
def updateRule(self, rule_name: str, **kwargs):
|
||||||
|
"""Update an existing rule."""
|
||||||
|
for rule in self.rules:
|
||||||
|
if rule.name == rule_name:
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if hasattr(rule, key):
|
||||||
|
setattr(rule, key, value)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# Global configuration instance
|
||||||
|
model_selection_config = ModelSelectionConfig()
|
||||||
279
modules/aicore/aicoreModelSelector.py
Normal file
279
modules/aicore/aicoreModelSelector.py
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
"""
|
||||||
|
Dynamic model selector using configurable rules and scoring.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import List, Optional, Dict, Any, Tuple
|
||||||
|
from modules.datamodels.datamodelAi import AiModel, AiCallOptions, OperationType, Priority, ProcessingMode, ModelTags
|
||||||
|
from modules.aicore.aicoreModelSelectionConfig import model_selection_config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelSelector:
|
||||||
|
"""Dynamic model selector using configurable rules."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.config = model_selection_config
|
||||||
|
|
||||||
|
def selectModel(self,
|
||||||
|
prompt: str,
|
||||||
|
context: str,
|
||||||
|
options: AiCallOptions,
|
||||||
|
available_models: List[AiModel]) -> Optional[AiModel]:
|
||||||
|
"""
|
||||||
|
Select the best model based on configurable rules and scoring.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: User prompt
|
||||||
|
context: Context data
|
||||||
|
options: AI call options
|
||||||
|
available_models: List of available models to choose from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Selected model or None if no suitable model found
|
||||||
|
"""
|
||||||
|
if not available_models:
|
||||||
|
logger.warning("No models available for selection")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(f"Selecting model for operation: {options.operationType}, priority: {options.priority}")
|
||||||
|
|
||||||
|
# Calculate input size
|
||||||
|
input_size = len(prompt.encode("utf-8")) + len(context.encode("utf-8"))
|
||||||
|
|
||||||
|
# Get applicable rules
|
||||||
|
rules = self.config.getRulesForOperation(options.operationType)
|
||||||
|
logger.debug(f"Found {len(rules)} applicable rules for {options.operationType}")
|
||||||
|
|
||||||
|
# Score each model
|
||||||
|
scored_models = []
|
||||||
|
for model in available_models:
|
||||||
|
if not model.isAvailable:
|
||||||
|
continue
|
||||||
|
|
||||||
|
score = self._calculateModelScore(model, input_size, options, rules)
|
||||||
|
if score > 0: # Only consider models with positive scores
|
||||||
|
scored_models.append((model, score))
|
||||||
|
logger.debug(f"Model {model.name}: score={score:.2f}")
|
||||||
|
|
||||||
|
if not scored_models:
|
||||||
|
logger.warning("No models passed the selection criteria, trying fallback criteria")
|
||||||
|
# Try fallback criteria
|
||||||
|
fallback_criteria = self.getFallbackCriteria(options.operationType)
|
||||||
|
return self._selectWithFallbackCriteria(available_models, fallback_criteria, input_size, options)
|
||||||
|
|
||||||
|
# Sort by score (highest first)
|
||||||
|
scored_models.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
selected_model = scored_models[0][0]
|
||||||
|
selected_score = scored_models[0][1]
|
||||||
|
|
||||||
|
logger.info(f"Selected model: {selected_model.name} (score: {selected_score:.2f})")
|
||||||
|
|
||||||
|
# Log selection details
|
||||||
|
self._logSelectionDetails(selected_model, input_size, options)
|
||||||
|
|
||||||
|
return selected_model
|
||||||
|
|
||||||
|
def _calculateModelScore(self,
|
||||||
|
model: AiModel,
|
||||||
|
input_size: int,
|
||||||
|
options: AiCallOptions,
|
||||||
|
rules: List) -> float:
|
||||||
|
"""Calculate score for a model based on rules and criteria."""
|
||||||
|
score = 0.0
|
||||||
|
|
||||||
|
# Check basic requirements
|
||||||
|
if not self._meetsBasicRequirements(model, input_size, options):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Apply rules
|
||||||
|
for rule in rules:
|
||||||
|
rule_score = self._applyRule(model, input_size, options, rule)
|
||||||
|
score += rule_score * rule.weight
|
||||||
|
|
||||||
|
# Apply priority-based scoring
|
||||||
|
priority_score = self._applyPriorityScoring(model, options)
|
||||||
|
score += priority_score
|
||||||
|
|
||||||
|
# Apply processing mode scoring
|
||||||
|
mode_score = self._applyProcessingModeScoring(model, options)
|
||||||
|
score += mode_score
|
||||||
|
|
||||||
|
# Apply cost constraints
|
||||||
|
if not self._meetsCostConstraints(model, input_size, options):
|
||||||
|
score *= 0.1 # Heavily penalize but don't eliminate
|
||||||
|
|
||||||
|
return max(0.0, score)
|
||||||
|
|
||||||
|
def _meetsBasicRequirements(self, model: AiModel, input_size: int, options: AiCallOptions) -> bool:
|
||||||
|
"""Check if model meets basic requirements."""
|
||||||
|
# Context length check
|
||||||
|
if model.contextLength > 0 and input_size > model.contextLength * 0.8:
|
||||||
|
logger.debug(f"Model {model.name} rejected: input too large ({input_size} > {model.contextLength * 0.8})")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Required tags check
|
||||||
|
if options.requiredTags:
|
||||||
|
if not all(tag in model.tags for tag in options.requiredTags):
|
||||||
|
logger.debug(f"Model {model.name} rejected: missing required tags")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Capabilities check
|
||||||
|
if options.modelCapabilities:
|
||||||
|
if not all(cap in model.capabilities for cap in options.modelCapabilities):
|
||||||
|
logger.debug(f"Model {model.name} rejected: missing required capabilities")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Avoid tags check
|
||||||
|
for rule in self.config.getRulesForOperation(options.operationType):
|
||||||
|
if any(tag in model.tags for tag in rule.avoid_tags):
|
||||||
|
logger.debug(f"Model {model.name} rejected: has avoid tags")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _applyRule(self, model: AiModel, input_size: int, options: AiCallOptions, rule) -> float:
|
||||||
|
"""Apply a specific rule to calculate score contribution."""
|
||||||
|
score = 0.0
|
||||||
|
|
||||||
|
# Required tags match
|
||||||
|
if all(tag in model.tags for tag in rule.required_tags):
|
||||||
|
score += 1.0
|
||||||
|
|
||||||
|
# Preferred tags match
|
||||||
|
preferred_matches = sum(1 for tag in rule.preferred_tags if tag in model.tags)
|
||||||
|
if rule.preferred_tags:
|
||||||
|
score += (preferred_matches / len(rule.preferred_tags)) * 0.5
|
||||||
|
|
||||||
|
# Quality rating check
|
||||||
|
if rule.min_quality_rating and model.qualityRating >= rule.min_quality_rating:
|
||||||
|
score += 0.3
|
||||||
|
|
||||||
|
# Context length check
|
||||||
|
if rule.min_context_length and model.contextLength >= rule.min_context_length:
|
||||||
|
score += 0.2
|
||||||
|
|
||||||
|
return score
|
||||||
|
|
||||||
|
def _applyPriorityScoring(self, model: AiModel, options: AiCallOptions) -> float:
|
||||||
|
"""Apply priority-based scoring."""
|
||||||
|
if options.priority == Priority.SPEED:
|
||||||
|
return model.speedRating * 0.1
|
||||||
|
elif options.priority == Priority.QUALITY:
|
||||||
|
return model.qualityRating * 0.1
|
||||||
|
elif options.priority == Priority.COST:
|
||||||
|
# Lower cost = higher score
|
||||||
|
cost_score = max(0, 1.0 - (model.costPer1kTokens * 1000))
|
||||||
|
return cost_score * 0.1
|
||||||
|
else: # BALANCED
|
||||||
|
return (model.qualityRating + model.speedRating) * 0.05
|
||||||
|
|
||||||
|
def _applyProcessingModeScoring(self, model: AiModel, options: AiCallOptions) -> float:
|
||||||
|
"""Apply processing mode scoring."""
|
||||||
|
if options.processingMode == ProcessingMode.DETAILED:
|
||||||
|
if ModelTags.HIGH_QUALITY in model.tags:
|
||||||
|
return 0.2
|
||||||
|
elif options.processingMode == ProcessingMode.BASIC:
|
||||||
|
if ModelTags.FAST in model.tags:
|
||||||
|
return 0.2
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _meetsCostConstraints(self, model: AiModel, input_size: int, options: AiCallOptions) -> bool:
|
||||||
|
"""Check if model meets cost constraints."""
|
||||||
|
if options.maxCost is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Estimate cost
|
||||||
|
estimated_tokens = input_size / 4
|
||||||
|
estimated_cost = (estimated_tokens / 1000) * model.costPer1kTokens
|
||||||
|
|
||||||
|
return estimated_cost <= options.maxCost
|
||||||
|
|
||||||
|
def _logSelectionDetails(self, model: AiModel, input_size: int, options: AiCallOptions):
|
||||||
|
"""Log detailed selection information."""
|
||||||
|
logger.info(f"Model Selection Details:")
|
||||||
|
logger.info(f" Selected: {model.displayName} ({model.name})")
|
||||||
|
logger.info(f" Connector: {model.connectorType}")
|
||||||
|
logger.info(f" Operation: {options.operationType}")
|
||||||
|
logger.info(f" Priority: {options.priority}")
|
||||||
|
logger.info(f" Processing Mode: {options.processingMode}")
|
||||||
|
logger.info(f" Input Size: {input_size} bytes")
|
||||||
|
logger.info(f" Context Length: {model.contextLength}")
|
||||||
|
logger.info(f" Max Tokens: {model.maxTokens}")
|
||||||
|
logger.info(f" Quality Rating: {model.qualityRating}/10")
|
||||||
|
logger.info(f" Speed Rating: {model.speedRating}/10")
|
||||||
|
logger.info(f" Cost: ${model.costPer1kTokens:.4f}/1k tokens")
|
||||||
|
logger.info(f" Capabilities: {', '.join(model.capabilities)}")
|
||||||
|
logger.info(f" Tags: {', '.join(model.tags)}")
|
||||||
|
|
||||||
|
def getFallbackCriteria(self, operation_type: str) -> Dict[str, Any]:
|
||||||
|
"""Get fallback selection criteria for an operation type."""
|
||||||
|
return self.config.getFallbackCriteria(operation_type)
|
||||||
|
|
||||||
|
def _selectWithFallbackCriteria(self,
|
||||||
|
available_models: List[AiModel],
|
||||||
|
fallback_criteria: Dict[str, Any],
|
||||||
|
input_size: int,
|
||||||
|
options: AiCallOptions) -> Optional[AiModel]:
|
||||||
|
"""Select model using fallback criteria when normal selection fails."""
|
||||||
|
logger.info("Using fallback criteria for model selection")
|
||||||
|
|
||||||
|
# Filter models by fallback criteria
|
||||||
|
candidates = []
|
||||||
|
for model in available_models:
|
||||||
|
if not model.isAvailable:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check required tags
|
||||||
|
if fallback_criteria.get("required_tags"):
|
||||||
|
if not all(tag in model.tags for tag in fallback_criteria["required_tags"]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check quality rating
|
||||||
|
if fallback_criteria.get("min_quality_rating"):
|
||||||
|
if model.qualityRating < fallback_criteria["min_quality_rating"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check cost
|
||||||
|
if fallback_criteria.get("max_cost_per_1k"):
|
||||||
|
if model.costPer1kTokens > fallback_criteria["max_cost_per_1k"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check context length
|
||||||
|
if model.contextLength > 0 and input_size > model.contextLength * 0.8:
|
||||||
|
continue
|
||||||
|
|
||||||
|
candidates.append(model)
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
logger.error("No models available even with fallback criteria")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sort by priority order from fallback criteria
|
||||||
|
priority_order = fallback_criteria.get("priority_order", ["quality", "speed", "cost"])
|
||||||
|
|
||||||
|
def get_priority_score(model: AiModel) -> float:
|
||||||
|
score = 0.0
|
||||||
|
for i, priority in enumerate(priority_order):
|
||||||
|
weight = len(priority_order) - i # Higher weight for earlier priorities
|
||||||
|
if priority == "quality":
|
||||||
|
score += model.qualityRating * weight
|
||||||
|
elif priority == "speed":
|
||||||
|
score += model.speedRating * weight
|
||||||
|
elif priority == "cost":
|
||||||
|
# Lower cost = higher score
|
||||||
|
score += (1.0 - model.costPer1kTokens * 1000) * weight
|
||||||
|
return score
|
||||||
|
|
||||||
|
candidates.sort(key=get_priority_score, reverse=True)
|
||||||
|
selected_model = candidates[0]
|
||||||
|
|
||||||
|
logger.info(f"Fallback selection: {selected_model.name} (score: {get_priority_score(selected_model):.2f})")
|
||||||
|
return selected_model
|
||||||
|
|
||||||
|
|
||||||
|
# Global selector instance
|
||||||
|
model_selector = ModelSelector()
|
||||||
|
|
@ -4,6 +4,8 @@ import os
|
||||||
from typing import Dict, Any, List, Union
|
from typing import Dict, Any, List, Union
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
|
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||||
|
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -17,10 +19,11 @@ def loadConfigData():
|
||||||
"temperature": float(APP_CONFIG.get('Connector_AiAnthropic_TEMPERATURE')),
|
"temperature": float(APP_CONFIG.get('Connector_AiAnthropic_TEMPERATURE')),
|
||||||
}
|
}
|
||||||
|
|
||||||
class AiAnthropic:
|
class AiAnthropic(BaseConnectorAi):
|
||||||
"""Connector for communication with the Anthropic API."""
|
"""Connector for communication with the Anthropic API."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
# Load configuration
|
# Load configuration
|
||||||
self.config = loadConfigData()
|
self.config = loadConfigData()
|
||||||
self.apiKey = self.config["apiKey"]
|
self.apiKey = self.config["apiKey"]
|
||||||
|
|
@ -39,25 +42,51 @@ class AiAnthropic:
|
||||||
|
|
||||||
logger.info(f"Anthropic Connector initialized with model: {self.modelName}")
|
logger.info(f"Anthropic Connector initialized with model: {self.modelName}")
|
||||||
|
|
||||||
def _getMaxTokensForModel(self, maxTokens: int = None) -> int:
|
def getConnectorType(self) -> str:
|
||||||
"""Get appropriate max_tokens for the current model."""
|
"""Get the connector type identifier."""
|
||||||
if maxTokens is not None:
|
return "anthropic"
|
||||||
return maxTokens
|
|
||||||
|
def getModels(self) -> List[AiModel]:
|
||||||
|
"""Get all available Anthropic models."""
|
||||||
|
return [
|
||||||
|
AiModel(
|
||||||
|
name="anthropic_callAiBasic",
|
||||||
|
displayName="Claude 3.5 Sonnet",
|
||||||
|
connectorType="anthropic",
|
||||||
|
maxTokens=200000,
|
||||||
|
contextLength=200000,
|
||||||
|
costPer1kTokens=0.015,
|
||||||
|
costPer1kTokensOutput=0.075,
|
||||||
|
speedRating=7,
|
||||||
|
qualityRating=10,
|
||||||
|
capabilities=["text_generation", "chat", "reasoning", "analysis"],
|
||||||
|
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.ANALYSIS, ModelTags.HIGH_QUALITY],
|
||||||
|
functionCall=self.callAiBasic,
|
||||||
|
priority="quality",
|
||||||
|
processingMode="detailed",
|
||||||
|
preferredFor=["generate_plan", "analyse_content"],
|
||||||
|
version="claude-3-5-sonnet-20241022"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="anthropic_callAiImage",
|
||||||
|
displayName="Claude 3.5 Sonnet Vision",
|
||||||
|
connectorType="anthropic",
|
||||||
|
maxTokens=200000,
|
||||||
|
contextLength=200000,
|
||||||
|
costPer1kTokens=0.015,
|
||||||
|
costPer1kTokensOutput=0.075,
|
||||||
|
speedRating=7,
|
||||||
|
qualityRating=10,
|
||||||
|
capabilities=["image_analysis", "vision", "multimodal"],
|
||||||
|
tags=[ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL, ModelTags.HIGH_QUALITY],
|
||||||
|
functionCall=self.callAiImage,
|
||||||
|
priority="quality",
|
||||||
|
processingMode="detailed",
|
||||||
|
preferredFor=["image_analysis"],
|
||||||
|
version="claude-3-5-sonnet-20241022"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
# Model-specific defaults based on Anthropic's limits
|
|
||||||
model_name = self.modelName.lower()
|
|
||||||
if "claude-3-5-sonnet" in model_name:
|
|
||||||
return 200000 # Claude 3.5 Sonnet max
|
|
||||||
elif "claude-3-5-haiku" in model_name:
|
|
||||||
return 200000 # Claude 3.5 Haiku max
|
|
||||||
elif "claude-3-opus" in model_name:
|
|
||||||
return 200000 # Claude 3 Opus max
|
|
||||||
elif "claude-3-sonnet" in model_name:
|
|
||||||
return 200000 # Claude 3 Sonnet max
|
|
||||||
elif "claude-3-haiku" in model_name:
|
|
||||||
return 200000 # Claude 3 Haiku max
|
|
||||||
else:
|
|
||||||
return 200000 # Default to maximum for unknown models
|
|
||||||
|
|
||||||
async def callAiBasic(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> Dict[str, Any]:
|
async def callAiBasic(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -126,8 +155,10 @@ class AiAnthropic:
|
||||||
"temperature": temperature,
|
"temperature": temperature,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Anthropic requires max_tokens - use model-appropriate value
|
# Anthropic requires max_tokens - use provided value or throw error
|
||||||
payload["max_tokens"] = self._getMaxTokensForModel(maxTokens)
|
if maxTokens is None:
|
||||||
|
raise ValueError("maxTokens must be provided for Anthropic API calls")
|
||||||
|
payload["max_tokens"] = maxTokens
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
payload["system"] = system_prompt
|
payload["system"] = system_prompt
|
||||||
|
|
||||||
|
|
@ -4,6 +4,8 @@ import httpx
|
||||||
from typing import Dict, Any, List, Union
|
from typing import Dict, Any, List, Union
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
|
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||||
|
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -21,10 +23,11 @@ def loadConfigData():
|
||||||
"temperature": float(APP_CONFIG.get('Connector_AiOpenai_TEMPERATURE')),
|
"temperature": float(APP_CONFIG.get('Connector_AiOpenai_TEMPERATURE')),
|
||||||
}
|
}
|
||||||
|
|
||||||
class AiOpenai:
|
class AiOpenai(BaseConnectorAi):
|
||||||
"""Connector for communication with the OpenAI API."""
|
"""Connector for communication with the OpenAI API."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
# Load configuration
|
# Load configuration
|
||||||
self.config = loadConfigData()
|
self.config = loadConfigData()
|
||||||
self.apiKey = self.config["apiKey"]
|
self.apiKey = self.config["apiKey"]
|
||||||
|
|
@ -41,6 +44,87 @@ class AiOpenai:
|
||||||
)
|
)
|
||||||
logger.info(f"OpenAI Connector initialized with model: {self.modelName}")
|
logger.info(f"OpenAI Connector initialized with model: {self.modelName}")
|
||||||
|
|
||||||
|
def getConnectorType(self) -> str:
|
||||||
|
"""Get the connector type identifier."""
|
||||||
|
return "openai"
|
||||||
|
|
||||||
|
def getModels(self) -> List[AiModel]:
|
||||||
|
"""Get all available OpenAI models."""
|
||||||
|
return [
|
||||||
|
AiModel(
|
||||||
|
name="openai_callAiBasic",
|
||||||
|
displayName="GPT-4o",
|
||||||
|
connectorType="openai",
|
||||||
|
maxTokens=128000,
|
||||||
|
contextLength=128000,
|
||||||
|
costPer1kTokens=0.03,
|
||||||
|
costPer1kTokensOutput=0.06,
|
||||||
|
speedRating=8,
|
||||||
|
qualityRating=9,
|
||||||
|
capabilities=["text_generation", "chat", "reasoning", "analysis"],
|
||||||
|
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.ANALYSIS],
|
||||||
|
functionCall=self.callAiBasic,
|
||||||
|
priority="balanced",
|
||||||
|
processingMode="advanced",
|
||||||
|
preferredFor=["general", "analyse_content"],
|
||||||
|
version="gpt-4o"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="openai_callAiBasic_gpt35",
|
||||||
|
displayName="GPT-3.5 Turbo",
|
||||||
|
connectorType="openai",
|
||||||
|
maxTokens=16000,
|
||||||
|
contextLength=16000,
|
||||||
|
costPer1kTokens=0.0015,
|
||||||
|
costPer1kTokensOutput=0.002,
|
||||||
|
speedRating=9,
|
||||||
|
qualityRating=7,
|
||||||
|
capabilities=["text_generation", "chat", "reasoning"],
|
||||||
|
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.FAST, ModelTags.COST_EFFECTIVE],
|
||||||
|
functionCall=self.callAiBasic,
|
||||||
|
priority="speed",
|
||||||
|
processingMode="basic",
|
||||||
|
preferredFor=["general"],
|
||||||
|
version="gpt-3.5-turbo"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="openai_callAiImage",
|
||||||
|
displayName="GPT-4o Vision",
|
||||||
|
connectorType="openai",
|
||||||
|
maxTokens=128000,
|
||||||
|
contextLength=128000,
|
||||||
|
costPer1kTokens=0.03,
|
||||||
|
costPer1kTokensOutput=0.06,
|
||||||
|
speedRating=7,
|
||||||
|
qualityRating=9,
|
||||||
|
capabilities=["image_analysis", "vision", "multimodal"],
|
||||||
|
tags=[ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL],
|
||||||
|
functionCall=self.callAiImage,
|
||||||
|
priority="quality",
|
||||||
|
processingMode="detailed",
|
||||||
|
preferredFor=["image_analysis"],
|
||||||
|
version="gpt-4o"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="openai_generateImage",
|
||||||
|
displayName="DALL-E 3",
|
||||||
|
connectorType="openai",
|
||||||
|
maxTokens=0, # Image generation doesn't use tokens
|
||||||
|
contextLength=0,
|
||||||
|
costPer1kTokens=0.04,
|
||||||
|
costPer1kTokensOutput=0.0,
|
||||||
|
speedRating=6,
|
||||||
|
qualityRating=9,
|
||||||
|
capabilities=["image_generation", "art", "visual_creation"],
|
||||||
|
tags=[ModelTags.IMAGE_GENERATION, ModelTags.ART, ModelTags.VISUAL],
|
||||||
|
functionCall=self.generateImage,
|
||||||
|
priority="quality",
|
||||||
|
processingMode="detailed",
|
||||||
|
preferredFor=["image_generation"],
|
||||||
|
version="dall-e-3"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
async def callAiBasic(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> str:
|
async def callAiBasic(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> str:
|
||||||
"""
|
"""
|
||||||
Calls the OpenAI API with the given messages.
|
Calls the OpenAI API with the given messages.
|
||||||
|
|
@ -70,9 +154,10 @@ class AiOpenai:
|
||||||
"temperature": temperature
|
"temperature": temperature
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only add max_tokens if it's explicitly set
|
# Add max_tokens - use provided value or throw error
|
||||||
if maxTokens is not None:
|
if maxTokens is None:
|
||||||
payload["max_tokens"] = maxTokens
|
raise ValueError("maxTokens must be provided for OpenAI API calls")
|
||||||
|
payload["max_tokens"] = maxTokens
|
||||||
|
|
||||||
response = await self.httpClient.post(
|
response = await self.httpClient.post(
|
||||||
self.apiUrl,
|
self.apiUrl,
|
||||||
|
|
@ -4,6 +4,8 @@ import asyncio
|
||||||
from typing import Dict, Any, List, Union, Optional
|
from typing import Dict, Any, List, Union, Optional
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
|
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||||
|
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -17,10 +19,11 @@ def loadConfigData():
|
||||||
"temperature": float(APP_CONFIG.get('Connector_AiPerplexity_TEMPERATURE')),
|
"temperature": float(APP_CONFIG.get('Connector_AiPerplexity_TEMPERATURE')),
|
||||||
}
|
}
|
||||||
|
|
||||||
class AiPerplexity:
|
class AiPerplexity(BaseConnectorAi):
|
||||||
"""Connector for communication with the Perplexity API."""
|
"""Connector for communication with the Perplexity API."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
# Load configuration
|
# Load configuration
|
||||||
self.config = loadConfigData()
|
self.config = loadConfigData()
|
||||||
self.apiKey = self.config["apiKey"]
|
self.apiKey = self.config["apiKey"]
|
||||||
|
|
@ -39,6 +42,105 @@ class AiPerplexity:
|
||||||
|
|
||||||
logger.info(f"Perplexity Connector initialized with model: {self.modelName}")
|
logger.info(f"Perplexity Connector initialized with model: {self.modelName}")
|
||||||
|
|
||||||
|
def getConnectorType(self) -> str:
|
||||||
|
"""Get the connector type identifier."""
|
||||||
|
return "perplexity"
|
||||||
|
|
||||||
|
def getModels(self) -> List[AiModel]:
|
||||||
|
"""Get all available Perplexity models."""
|
||||||
|
return [
|
||||||
|
AiModel(
|
||||||
|
name="perplexity_callAiBasic",
|
||||||
|
displayName="Llama 3.1 Sonar Large 128k",
|
||||||
|
connectorType="perplexity",
|
||||||
|
maxTokens=128000,
|
||||||
|
contextLength=128000,
|
||||||
|
costPer1kTokens=0.005,
|
||||||
|
costPer1kTokensOutput=0.005,
|
||||||
|
speedRating=8,
|
||||||
|
qualityRating=8,
|
||||||
|
capabilities=["text_generation", "chat", "reasoning", "web_search"],
|
||||||
|
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.WEB, ModelTags.SEARCH, ModelTags.COST_EFFECTIVE],
|
||||||
|
functionCall=self.callAiBasic,
|
||||||
|
priority="balanced",
|
||||||
|
processingMode="advanced",
|
||||||
|
preferredFor=["general", "web_research"],
|
||||||
|
version="llama-3.1-sonar-large-128k-online"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="perplexity_callAiWithWebSearch",
|
||||||
|
displayName="Sonar Pro",
|
||||||
|
connectorType="perplexity",
|
||||||
|
maxTokens=128000,
|
||||||
|
contextLength=128000,
|
||||||
|
costPer1kTokens=0.01,
|
||||||
|
costPer1kTokensOutput=0.01,
|
||||||
|
speedRating=7,
|
||||||
|
qualityRating=9,
|
||||||
|
capabilities=["text_generation", "web_search", "research"],
|
||||||
|
tags=[ModelTags.TEXT, ModelTags.WEB, ModelTags.SEARCH, ModelTags.RESEARCH, ModelTags.HIGH_QUALITY],
|
||||||
|
functionCall=self.callAiWithWebSearch,
|
||||||
|
priority="quality",
|
||||||
|
processingMode="detailed",
|
||||||
|
preferredFor=["web_research"],
|
||||||
|
version="sonar-pro"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="perplexity_researchTopic",
|
||||||
|
displayName="Mistral 7B Instruct",
|
||||||
|
connectorType="perplexity",
|
||||||
|
maxTokens=32000,
|
||||||
|
contextLength=32000,
|
||||||
|
costPer1kTokens=0.002,
|
||||||
|
costPer1kTokensOutput=0.002,
|
||||||
|
speedRating=8,
|
||||||
|
qualityRating=8,
|
||||||
|
capabilities=["web_search", "research", "information_gathering"],
|
||||||
|
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.RESEARCH, ModelTags.INFORMATION, ModelTags.COST_EFFECTIVE],
|
||||||
|
functionCall=self.researchTopic,
|
||||||
|
priority="cost",
|
||||||
|
processingMode="basic",
|
||||||
|
preferredFor=["web_research"],
|
||||||
|
version="mistral-7b-instruct"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="perplexity_answerQuestion",
|
||||||
|
displayName="Mistral 7B Instruct QA",
|
||||||
|
connectorType="perplexity",
|
||||||
|
maxTokens=32000,
|
||||||
|
contextLength=32000,
|
||||||
|
costPer1kTokens=0.002,
|
||||||
|
costPer1kTokensOutput=0.002,
|
||||||
|
speedRating=8,
|
||||||
|
qualityRating=8,
|
||||||
|
capabilities=["web_search", "question_answering", "research"],
|
||||||
|
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.RESEARCH, ModelTags.COST_EFFECTIVE],
|
||||||
|
functionCall=self.answerQuestion,
|
||||||
|
priority="cost",
|
||||||
|
processingMode="basic",
|
||||||
|
preferredFor=["web_research"],
|
||||||
|
version="mistral-7b-instruct"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="perplexity_getCurrentNews",
|
||||||
|
displayName="Mistral 7B Instruct News",
|
||||||
|
connectorType="perplexity",
|
||||||
|
maxTokens=32000,
|
||||||
|
contextLength=32000,
|
||||||
|
costPer1kTokens=0.002,
|
||||||
|
costPer1kTokensOutput=0.002,
|
||||||
|
speedRating=8,
|
||||||
|
qualityRating=8,
|
||||||
|
capabilities=["web_search", "news", "current_events"],
|
||||||
|
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.COST_EFFECTIVE],
|
||||||
|
functionCall=self.getCurrentNews,
|
||||||
|
priority="cost",
|
||||||
|
processingMode="basic",
|
||||||
|
preferredFor=["web_research"],
|
||||||
|
version="mistral-7b-instruct"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
async def callAiBasic(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> str:
|
async def callAiBasic(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> str:
|
||||||
"""
|
"""
|
||||||
Calls the Perplexity API with the given messages.
|
Calls the Perplexity API with the given messages.
|
||||||
|
|
@ -68,8 +170,10 @@ class AiPerplexity:
|
||||||
"temperature": temperature
|
"temperature": temperature
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add max_tokens - use provided value or default to 128000 (Perplexity's typical limit)
|
# Add max_tokens - use provided value or throw error
|
||||||
payload["max_tokens"] = maxTokens if maxTokens is not None else 128000
|
if maxTokens is None:
|
||||||
|
raise ValueError("maxTokens must be provided for Perplexity API calls")
|
||||||
|
payload["max_tokens"] = maxTokens
|
||||||
|
|
||||||
response = await self.httpClient.post(
|
response = await self.httpClient.post(
|
||||||
self.apiUrl,
|
self.apiUrl,
|
||||||
|
|
@ -134,8 +238,10 @@ class AiPerplexity:
|
||||||
"temperature": temperature
|
"temperature": temperature
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add max_tokens - use provided value or default to 128000 (Perplexity's typical limit)
|
# Add max_tokens - use provided value or throw error
|
||||||
payload["max_tokens"] = maxTokens if maxTokens is not None else 128000
|
if maxTokens is None:
|
||||||
|
raise ValueError("maxTokens must be provided for Perplexity API calls")
|
||||||
|
payload["max_tokens"] = maxTokens
|
||||||
|
|
||||||
response = await self.httpClient.post(
|
response = await self.httpClient.post(
|
||||||
self.apiUrl,
|
self.apiUrl,
|
||||||
|
|
@ -4,10 +4,12 @@
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from tavily import AsyncTavilyClient
|
from tavily import AsyncTavilyClient
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
from modules.shared.timezoneUtils import get_utc_timestamp
|
from modules.shared.timezoneUtils import get_utc_timestamp
|
||||||
|
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||||
|
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||||
from modules.datamodels.datamodelWeb import (
|
from modules.datamodels.datamodelWeb import (
|
||||||
WebSearchActionResult,
|
WebSearchActionResult,
|
||||||
WebSearchActionDocument,
|
WebSearchActionDocument,
|
||||||
|
|
@ -37,16 +39,100 @@ class WebCrawlResult:
|
||||||
url: str
|
url: str
|
||||||
content: str
|
content: str
|
||||||
|
|
||||||
@dataclass
|
class ConnectorWeb(BaseConnectorAi):
|
||||||
class ConnectorWeb:
|
"""Tavily web search connector."""
|
||||||
client: AsyncTavilyClient = None
|
|
||||||
# Cached settings loaded at initialization time
|
def __init__(self):
|
||||||
crawl_timeout: int = 30
|
super().__init__()
|
||||||
crawl_max_retries: int = 3
|
self.client: Optional[AsyncTavilyClient] = None
|
||||||
crawl_retry_delay: int = 2
|
# Cached settings loaded at initialization time
|
||||||
# Cached web search constraints (camelCase per project style)
|
self.crawl_timeout: int = 30
|
||||||
webSearchMinResults: int = 1
|
self.crawl_max_retries: int = 3
|
||||||
webSearchMaxResults: int = 20
|
self.crawl_retry_delay: int = 2
|
||||||
|
# Cached web search constraints (camelCase per project style)
|
||||||
|
self.webSearchMinResults: int = 1
|
||||||
|
self.webSearchMaxResults: int = 20
|
||||||
|
|
||||||
|
def getConnectorType(self) -> str:
|
||||||
|
"""Get the connector type identifier."""
|
||||||
|
return "tavily"
|
||||||
|
|
||||||
|
def getModels(self) -> List[AiModel]:
|
||||||
|
"""Get all available Tavily models."""
|
||||||
|
return [
|
||||||
|
AiModel(
|
||||||
|
name="tavily_search",
|
||||||
|
displayName="Tavily Search",
|
||||||
|
connectorType="tavily",
|
||||||
|
maxTokens=0, # Web search doesn't use tokens
|
||||||
|
contextLength=0,
|
||||||
|
costPer1kTokens=0.0,
|
||||||
|
costPer1kTokensOutput=0.0,
|
||||||
|
speedRating=8,
|
||||||
|
qualityRating=8,
|
||||||
|
capabilities=["web_search", "information_retrieval", "url_discovery"],
|
||||||
|
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.INFORMATION],
|
||||||
|
functionCall=self.search,
|
||||||
|
priority="balanced",
|
||||||
|
processingMode="basic",
|
||||||
|
preferredFor=["web_research"],
|
||||||
|
version="tavily-search"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="tavily_extract",
|
||||||
|
displayName="Tavily Extract",
|
||||||
|
connectorType="tavily",
|
||||||
|
maxTokens=0, # Web extraction doesn't use tokens
|
||||||
|
contextLength=0,
|
||||||
|
costPer1kTokens=0.0,
|
||||||
|
costPer1kTokensOutput=0.0,
|
||||||
|
speedRating=6,
|
||||||
|
qualityRating=8,
|
||||||
|
capabilities=["web_crawling", "content_extraction", "text_extraction"],
|
||||||
|
tags=[ModelTags.WEB, ModelTags.EXTRACT, ModelTags.CONTENT],
|
||||||
|
functionCall=self.crawl,
|
||||||
|
priority="balanced",
|
||||||
|
processingMode="basic",
|
||||||
|
preferredFor=["web_research"],
|
||||||
|
version="tavily-extract"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="tavily_crawl",
|
||||||
|
displayName="Tavily Crawl",
|
||||||
|
connectorType="tavily",
|
||||||
|
maxTokens=0, # Web crawling doesn't use tokens
|
||||||
|
contextLength=0,
|
||||||
|
costPer1kTokens=0.0,
|
||||||
|
costPer1kTokensOutput=0.0,
|
||||||
|
speedRating=6,
|
||||||
|
qualityRating=8,
|
||||||
|
capabilities=["web_crawling", "content_extraction", "mapping"],
|
||||||
|
tags=[ModelTags.WEB, ModelTags.CRAWL, ModelTags.EXTRACT],
|
||||||
|
functionCall=self.crawl,
|
||||||
|
priority="balanced",
|
||||||
|
processingMode="basic",
|
||||||
|
preferredFor=["web_research"],
|
||||||
|
version="tavily-crawl"
|
||||||
|
),
|
||||||
|
AiModel(
|
||||||
|
name="tavily_scrape",
|
||||||
|
displayName="Tavily Scrape",
|
||||||
|
connectorType="tavily",
|
||||||
|
maxTokens=0, # Web scraping doesn't use tokens
|
||||||
|
contextLength=0,
|
||||||
|
costPer1kTokens=0.0,
|
||||||
|
costPer1kTokensOutput=0.0,
|
||||||
|
speedRating=6,
|
||||||
|
qualityRating=8,
|
||||||
|
capabilities=["web_search", "web_crawling", "content_extraction", "information_retrieval"],
|
||||||
|
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.CRAWL, ModelTags.EXTRACT, ModelTags.CONTENT, ModelTags.INFORMATION],
|
||||||
|
functionCall=self.scrape,
|
||||||
|
priority="balanced",
|
||||||
|
processingMode="basic",
|
||||||
|
preferredFor=["web_research"],
|
||||||
|
version="tavily-search-extract"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(cls):
|
async def create(cls):
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Optional, List, Dict, Any, Literal
|
from typing import Optional, List, Dict, Any, Literal, Callable
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -81,6 +81,52 @@ PROCESSING_MODE_PRIORITY_MAPPING = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AiModel(BaseModel):
|
||||||
|
"""Enhanced AI model definition with dynamic capabilities."""
|
||||||
|
|
||||||
|
# Core identification
|
||||||
|
name: str = Field(description="Unique model identifier")
|
||||||
|
displayName: str = Field(description="Human-readable model name")
|
||||||
|
connectorType: str = Field(description="Type of connector (openai, anthropic, perplexity, tavily, etc.)")
|
||||||
|
|
||||||
|
# Token and context limits
|
||||||
|
maxTokens: int = Field(description="Maximum tokens this model can generate")
|
||||||
|
contextLength: int = Field(description="Maximum context length this model can handle")
|
||||||
|
|
||||||
|
# Cost information
|
||||||
|
costPer1kTokens: float = Field(default=0.0, description="Cost per 1000 input tokens")
|
||||||
|
costPer1kTokensOutput: float = Field(default=0.0, description="Cost per 1000 output tokens")
|
||||||
|
|
||||||
|
# Performance ratings
|
||||||
|
speedRating: int = Field(ge=1, le=10, description="Speed rating (1-10, higher = faster)")
|
||||||
|
qualityRating: int = Field(ge=1, le=10, description="Quality rating (1-10, higher = better)")
|
||||||
|
|
||||||
|
# Capabilities and tags
|
||||||
|
capabilities: List[str] = Field(description="List of model capabilities")
|
||||||
|
tags: List[str] = Field(description="List of model tags for filtering")
|
||||||
|
|
||||||
|
# Function reference (not serialized)
|
||||||
|
functionCall: Optional[Callable] = Field(default=None, exclude=True, description="Function to call for this model")
|
||||||
|
|
||||||
|
# Selection criteria
|
||||||
|
priority: str = Field(default="balanced", description="Default priority for this model")
|
||||||
|
processingMode: str = Field(default="basic", description="Default processing mode")
|
||||||
|
isAvailable: bool = Field(default=True, description="Whether model is currently available")
|
||||||
|
|
||||||
|
# Advanced selection criteria
|
||||||
|
minContextLength: Optional[int] = Field(default=None, description="Minimum context length required")
|
||||||
|
maxCost: Optional[float] = Field(default=None, description="Maximum cost this model should be used for")
|
||||||
|
preferredFor: List[str] = Field(default=[], description="Operation types this model is preferred for")
|
||||||
|
avoidFor: List[str] = Field(default=[], description="Operation types this model should avoid")
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
version: Optional[str] = Field(default=None, description="Model version")
|
||||||
|
lastUpdated: Optional[str] = Field(default=None, description="Last update timestamp")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
arbitrary_types_allowed = True # Allow Callable type
|
||||||
|
|
||||||
|
|
||||||
class ModelCapabilities(BaseModel):
|
class ModelCapabilities(BaseModel):
|
||||||
"""Model capabilities and characteristics for dynamic selection."""
|
"""Model capabilities and characteristics for dynamic selection."""
|
||||||
|
|
||||||
|
|
|
||||||
1372
modules/interfaces/_BACKUP_NOT_USED_interfaceAiObjects.py
Normal file
1372
modules/interfaces/_BACKUP_NOT_USED_interfaceAiObjects.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -6,10 +6,9 @@ import time
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from modules.connectors.connectorAiOpenai import AiOpenai
|
# No more hardcoded imports - everything is discovered dynamically
|
||||||
from modules.connectors.connectorAiAnthropic import AiAnthropic
|
from modules.aicore.aicoreModelRegistry import model_registry
|
||||||
from modules.connectors.connectorAiPerplexity import AiPerplexity
|
from modules.aicore.aicoreModelSelector import model_selector
|
||||||
from modules.connectors.connectorAiTavily import ConnectorWeb
|
|
||||||
from modules.datamodels.datamodelAi import (
|
from modules.datamodels.datamodelAi import (
|
||||||
AiCallOptions,
|
AiCallOptions,
|
||||||
AiCallRequest,
|
AiCallRequest,
|
||||||
|
|
@ -32,461 +31,62 @@ from modules.datamodels.datamodelWeb import (
|
||||||
from modules.datamodels.datamodelChat import ActionDocument
|
from modules.datamodels.datamodelChat import ActionDocument
|
||||||
|
|
||||||
|
|
||||||
# Comprehensive model registry with capability tags and function mapping
|
# Dynamic model registry - models are now loaded from connectors via aicore system
|
||||||
aiModels: Dict[str, Dict[str, Any]] = {
|
|
||||||
# OpenAI Models
|
|
||||||
"openai_callAiBasic": {
|
|
||||||
"connector": "openai",
|
|
||||||
"function": "callAiBasic",
|
|
||||||
"llmName": "gpt-4o",
|
|
||||||
"contextLength": 128000,
|
|
||||||
"costPer1kTokens": 0.03,
|
|
||||||
"costPer1kTokensOutput": 0.06,
|
|
||||||
"speedRating": 8,
|
|
||||||
"qualityRating": 9,
|
|
||||||
"capabilities": ["text_generation", "chat", "reasoning", "analysis"],
|
|
||||||
"tags": ["text", "chat", "reasoning", "analysis", "general"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
|
|
||||||
},
|
|
||||||
"openai_callAiBasic_gpt35": {
|
|
||||||
"connector": "openai",
|
|
||||||
"function": "callAiBasic",
|
|
||||||
"llmName": "gpt-3.5-turbo",
|
|
||||||
"contextLength": 16000,
|
|
||||||
"costPer1kTokens": 0.0015,
|
|
||||||
"costPer1kTokensOutput": 0.002,
|
|
||||||
"speedRating": 9,
|
|
||||||
"qualityRating": 7,
|
|
||||||
"capabilities": ["text_generation", "chat", "reasoning"],
|
|
||||||
"tags": ["text", "chat", "reasoning", "general", "fast"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0015 + (bytesReceived / 4 / 1000) * 0.002
|
|
||||||
},
|
|
||||||
"openai_callAiImage": {
|
|
||||||
"connector": "openai",
|
|
||||||
"function": "callAiImage",
|
|
||||||
"llmName": "gpt-4o",
|
|
||||||
"contextLength": 128000,
|
|
||||||
"costPer1kTokens": 0.03,
|
|
||||||
"costPer1kTokensOutput": 0.06,
|
|
||||||
"speedRating": 7,
|
|
||||||
"qualityRating": 9,
|
|
||||||
"capabilities": ["image_analysis", "vision", "multimodal"],
|
|
||||||
"tags": ["image", "vision", "multimodal"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
|
|
||||||
},
|
|
||||||
"openai_generateImage": {
|
|
||||||
"connector": "openai",
|
|
||||||
"function": "generateImage",
|
|
||||||
"llmName": "dall-e-3",
|
|
||||||
"contextLength": 0,
|
|
||||||
"costPer1kTokens": 0.04,
|
|
||||||
"costPer1kTokensOutput": 0.0,
|
|
||||||
"speedRating": 6,
|
|
||||||
"qualityRating": 9,
|
|
||||||
"capabilities": ["image_generation", "art", "visual_creation"],
|
|
||||||
"tags": ["image_generation", "art", "visual"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04
|
|
||||||
},
|
|
||||||
|
|
||||||
# Anthropic Models
|
|
||||||
"anthropic_callAiBasic": {
|
|
||||||
"connector": "anthropic",
|
|
||||||
"function": "callAiBasic",
|
|
||||||
"llmName": "claude-3-5-sonnet-20241022",
|
|
||||||
"contextLength": 200000,
|
|
||||||
"costPer1kTokens": 0.015,
|
|
||||||
"costPer1kTokensOutput": 0.075,
|
|
||||||
"speedRating": 7,
|
|
||||||
"qualityRating": 10,
|
|
||||||
"capabilities": ["text_generation", "chat", "reasoning", "analysis"],
|
|
||||||
"tags": ["text", "chat", "reasoning", "analysis", "high_quality"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
|
|
||||||
},
|
|
||||||
"anthropic_callAiImage": {
|
|
||||||
"connector": "anthropic",
|
|
||||||
"function": "callAiImage",
|
|
||||||
"llmName": "claude-3-5-sonnet-20241022",
|
|
||||||
"contextLength": 200000,
|
|
||||||
"costPer1kTokens": 0.015,
|
|
||||||
"costPer1kTokensOutput": 0.075,
|
|
||||||
"speedRating": 7,
|
|
||||||
"qualityRating": 10,
|
|
||||||
"capabilities": ["image_analysis", "vision", "multimodal"],
|
|
||||||
"tags": ["image", "vision", "multimodal", "high_quality"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
|
|
||||||
},
|
|
||||||
|
|
||||||
# Perplexity Models
|
|
||||||
"perplexity_callAiBasic": {
|
|
||||||
"connector": "perplexity",
|
|
||||||
"function": "callAiBasic",
|
|
||||||
"llmName": "llama-3.1-sonar-large-128k-online",
|
|
||||||
"contextLength": 128000,
|
|
||||||
"costPer1kTokens": 0.005,
|
|
||||||
"costPer1kTokensOutput": 0.005,
|
|
||||||
"speedRating": 8,
|
|
||||||
"qualityRating": 8,
|
|
||||||
"capabilities": ["text_generation", "chat", "reasoning", "web_search"],
|
|
||||||
"tags": ["text", "chat", "reasoning", "web_search", "cost_effective"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005
|
|
||||||
},
|
|
||||||
"perplexity_callAiWithWebSearch": {
|
|
||||||
"connector": "perplexity",
|
|
||||||
"function": "callAiWithWebSearch",
|
|
||||||
"llmName": "sonar-pro",
|
|
||||||
"contextLength": 128000,
|
|
||||||
"costPer1kTokens": 0.01,
|
|
||||||
"costPer1kTokensOutput": 0.01,
|
|
||||||
"speedRating": 7,
|
|
||||||
"qualityRating": 9,
|
|
||||||
"capabilities": ["text_generation", "web_search", "research"],
|
|
||||||
"tags": ["text", "web_search", "research", "high_quality"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01
|
|
||||||
},
|
|
||||||
"perplexity_researchTopic": {
|
|
||||||
"connector": "perplexity",
|
|
||||||
"function": "researchTopic",
|
|
||||||
"llmName": "mistral-7b-instruct",
|
|
||||||
"contextLength": 32000,
|
|
||||||
"costPer1kTokens": 0.002,
|
|
||||||
"costPer1kTokensOutput": 0.002,
|
|
||||||
"speedRating": 8,
|
|
||||||
"qualityRating": 8,
|
|
||||||
"capabilities": ["web_search", "research", "information_gathering"],
|
|
||||||
"tags": ["web_search", "research", "information", "cost_effective"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
|
|
||||||
},
|
|
||||||
"perplexity_answerQuestion": {
|
|
||||||
"connector": "perplexity",
|
|
||||||
"function": "answerQuestion",
|
|
||||||
"llmName": "mistral-7b-instruct",
|
|
||||||
"contextLength": 32000,
|
|
||||||
"costPer1kTokens": 0.002,
|
|
||||||
"costPer1kTokensOutput": 0.002,
|
|
||||||
"speedRating": 8,
|
|
||||||
"qualityRating": 8,
|
|
||||||
"capabilities": ["web_search", "question_answering", "research"],
|
|
||||||
"tags": ["web_search", "qa", "research", "cost_effective"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
|
|
||||||
},
|
|
||||||
"perplexity_getCurrentNews": {
|
|
||||||
"connector": "perplexity",
|
|
||||||
"function": "getCurrentNews",
|
|
||||||
"llmName": "mistral-7b-instruct",
|
|
||||||
"contextLength": 32000,
|
|
||||||
"costPer1kTokens": 0.002,
|
|
||||||
"costPer1kTokensOutput": 0.002,
|
|
||||||
"speedRating": 8,
|
|
||||||
"qualityRating": 8,
|
|
||||||
"capabilities": ["web_search", "news", "current_events"],
|
|
||||||
"tags": ["web_search", "news", "current_events", "cost_effective"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
|
|
||||||
},
|
|
||||||
|
|
||||||
# Tavily Web Models
|
|
||||||
"tavily_search": {
|
|
||||||
"connector": "tavily",
|
|
||||||
"function": "search",
|
|
||||||
"llmName": "tavily-search",
|
|
||||||
"contextLength": 0,
|
|
||||||
"costPer1kTokens": 0.0, # Not token-based
|
|
||||||
"costPer1kTokensOutput": 0.0, # Not token-based
|
|
||||||
"speedRating": 8,
|
|
||||||
"qualityRating": 8,
|
|
||||||
"capabilities": ["web_search", "information_retrieval", "url_discovery"],
|
|
||||||
"tags": ["web", "search", "urls", "information"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived, searchDepth="basic", numRequests=1: (
|
|
||||||
# Basic search: 1 credit, Advanced: 2 credits
|
|
||||||
# Cost per credit: $0.008
|
|
||||||
numRequests * (1 if searchDepth == "basic" else 2) * 0.008
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"tavily_extract": {
|
|
||||||
"connector": "tavily",
|
|
||||||
"function": "extract",
|
|
||||||
"llmName": "tavily-extract",
|
|
||||||
"contextLength": 0,
|
|
||||||
"costPer1kTokens": 0.0,
|
|
||||||
"costPer1kTokensOutput": 0.0,
|
|
||||||
"speedRating": 6,
|
|
||||||
"qualityRating": 8,
|
|
||||||
"capabilities": ["web_crawling", "content_extraction", "text_extraction"],
|
|
||||||
"tags": ["web", "extract", "content"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived, extractionDepth="basic", numSuccessfulUrls=1: (
|
|
||||||
# Basic: 1 credit per 5 URLs, Advanced: 2 credits per 5 URLs
|
|
||||||
# Only charged for successful extractions
|
|
||||||
(numSuccessfulUrls / 5) * (1 if extractionDepth == "basic" else 2) * 0.008
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"tavily_crawl": {
|
|
||||||
"connector": "tavily",
|
|
||||||
"function": "crawl",
|
|
||||||
"llmName": "tavily-crawl",
|
|
||||||
"contextLength": 0,
|
|
||||||
"costPer1kTokens": 0.0,
|
|
||||||
"costPer1kTokensOutput": 0.0,
|
|
||||||
"speedRating": 6,
|
|
||||||
"qualityRating": 8,
|
|
||||||
"capabilities": ["web_crawling", "content_extraction", "mapping"],
|
|
||||||
"tags": ["web", "crawl", "map", "extract"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived, numPages=10, extractionDepth="basic", withInstructions=False, numSuccessfulExtractions=10: (
|
|
||||||
# Crawl = Mapping + Extraction
|
|
||||||
# Mapping: 1 credit per 10 pages (2 if with instructions)
|
|
||||||
# Extraction: 1 credit per 5 successful extractions (2 if advanced)
|
|
||||||
((numPages / 10) * (2 if withInstructions else 1) +
|
|
||||||
(numSuccessfulExtractions / 5) * (1 if extractionDepth == "basic" else 2)) * 0.008
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"tavily_scrape": {
|
|
||||||
"connector": "tavily",
|
|
||||||
"function": "scrape",
|
|
||||||
"llmName": "tavily-search-extract",
|
|
||||||
"contextLength": 0,
|
|
||||||
"costPer1kTokens": 0.0,
|
|
||||||
"costPer1kTokensOutput": 0.0,
|
|
||||||
"speedRating": 6,
|
|
||||||
"qualityRating": 8,
|
|
||||||
"capabilities": ["web_search", "web_crawling", "content_extraction", "information_retrieval"],
|
|
||||||
"tags": ["web", "search", "crawl", "extract", "content", "information"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived, searchDepth="basic", numSuccessfulUrls=1, extractionDepth="basic": (
|
|
||||||
# Combines search + extraction
|
|
||||||
# Search cost + extraction cost
|
|
||||||
(1 if searchDepth == "basic" else 2) +
|
|
||||||
(numSuccessfulUrls / 5) * (1 if extractionDepth == "basic" else 2)
|
|
||||||
) * 0.008
|
|
||||||
},
|
|
||||||
|
|
||||||
# Internal Models
|
|
||||||
"internal_extraction": {
|
|
||||||
"connector": "internal",
|
|
||||||
"function": "extract",
|
|
||||||
"llmName": "internal-extractor",
|
|
||||||
"contextLength": 0,
|
|
||||||
"costPer1kTokens": 0.0,
|
|
||||||
"costPer1kTokensOutput": 0.0,
|
|
||||||
"speedRating": 8,
|
|
||||||
"qualityRating": 8,
|
|
||||||
"capabilities": ["document_extraction", "content_processing"],
|
|
||||||
"tags": ["internal", "extraction", "document_processing"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: 0.001 + (bytesSent + bytesReceived) / (1024 * 1024) * 0.01 # $0.001 base + $0.01/MB
|
|
||||||
},
|
|
||||||
"internal_generation": {
|
|
||||||
"connector": "internal",
|
|
||||||
"function": "generate",
|
|
||||||
"llmName": "internal-generator",
|
|
||||||
"contextLength": 0,
|
|
||||||
"costPer1kTokens": 0.0,
|
|
||||||
"costPer1kTokensOutput": 0.0,
|
|
||||||
"speedRating": 7,
|
|
||||||
"qualityRating": 8,
|
|
||||||
"capabilities": ["document_generation", "content_creation"],
|
|
||||||
"tags": ["internal", "generation", "document_creation"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: 0.002 + (bytesReceived / (1024 * 1024)) * 0.005 # $0.002 base + $0.005/MB output
|
|
||||||
},
|
|
||||||
"internal_rendering": {
|
|
||||||
"connector": "internal",
|
|
||||||
"function": "render",
|
|
||||||
"llmName": "internal-renderer",
|
|
||||||
"contextLength": 0,
|
|
||||||
"costPer1kTokens": 0.0,
|
|
||||||
"costPer1kTokensOutput": 0.0,
|
|
||||||
"speedRating": 6,
|
|
||||||
"qualityRating": 9,
|
|
||||||
"capabilities": ["document_rendering", "format_conversion"],
|
|
||||||
"tags": ["internal", "rendering", "format_conversion"],
|
|
||||||
"calculatePriceUsd": lambda processingTime, bytesSent, bytesReceived: 0.003 + (bytesReceived / (1024 * 1024)) * 0.008 # $0.003 base + $0.008/MB output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class AiObjects:
|
class AiObjects:
|
||||||
"""Centralized AI interface: selects model and calls connector. Includes web functionality."""
|
"""Centralized AI interface: dynamically discovers and uses AI models. Includes web functionality."""
|
||||||
|
|
||||||
openaiService: AiOpenai
|
|
||||||
anthropicService: AiAnthropic
|
|
||||||
perplexityService: AiPerplexity
|
|
||||||
tavilyService: ConnectorWeb
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.openaiService is None:
|
# Auto-discover and register all available connectors
|
||||||
raise TypeError("openaiService must be provided")
|
self._discoverAndRegisterConnectors()
|
||||||
if self.anthropicService is None:
|
|
||||||
raise TypeError("anthropicService must be provided")
|
def _discoverAndRegisterConnectors(self):
|
||||||
if self.perplexityService is None:
|
"""Auto-discover and register all available AI connectors."""
|
||||||
raise TypeError("perplexityService must be provided")
|
logger.info("Auto-discovering AI connectors...")
|
||||||
if self.tavilyService is None:
|
|
||||||
raise TypeError("tavilyService must be provided")
|
# Use the model registry's built-in discovery mechanism
|
||||||
|
discovered_connectors = model_registry.discoverConnectors()
|
||||||
|
|
||||||
|
# Register each discovered connector
|
||||||
|
for connector in discovered_connectors:
|
||||||
|
model_registry.registerConnector(connector)
|
||||||
|
logger.info(f"Registered connector: {connector.getConnectorType()}")
|
||||||
|
|
||||||
|
logger.info(f"Total connectors registered: {len(discovered_connectors)}")
|
||||||
|
logger.info("All AI connectors registered with dynamic model registry")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(cls) -> "AiObjects":
|
async def create(cls) -> "AiObjects":
|
||||||
"""Create AiObjects instance with all connectors initialized."""
|
"""Create AiObjects instance with auto-discovered connectors."""
|
||||||
openaiService = AiOpenai()
|
# No need to manually create connectors - they're auto-discovered
|
||||||
anthropicService = AiAnthropic()
|
return cls()
|
||||||
perplexityService = AiPerplexity()
|
|
||||||
tavilyService = await ConnectorWeb.create()
|
|
||||||
|
|
||||||
return cls(
|
|
||||||
openaiService=openaiService,
|
|
||||||
anthropicService=anthropicService,
|
|
||||||
perplexityService=perplexityService,
|
|
||||||
tavilyService=tavilyService
|
|
||||||
)
|
|
||||||
|
|
||||||
def _estimateCost(self, modelInfo: Dict[str, Any], contentSize: int) -> float:
|
|
||||||
estimatedTokens = contentSize / 4
|
|
||||||
inputCost = (estimatedTokens / 1000) * modelInfo["costPer1kTokens"]
|
|
||||||
outputCost = (estimatedTokens / 1000) * modelInfo["costPer1kTokensOutput"] * 0.1
|
|
||||||
return inputCost + outputCost
|
|
||||||
|
|
||||||
|
|
||||||
def _selectModel(self, prompt: str, context: str, options: AiCallOptions) -> str:
|
def _selectModel(self, prompt: str, context: str, options: AiCallOptions) -> str:
|
||||||
"""Select the best model based on operation type, tags, and requirements."""
|
"""Select the best model using dynamic model selection system."""
|
||||||
totalSize = len(prompt.encode("utf-8")) + len(context.encode("utf-8"))
|
# Get available models from the dynamic registry
|
||||||
candidates: Dict[str, Dict[str, Any]] = {}
|
available_models = model_registry.getAvailableModels()
|
||||||
|
|
||||||
# Determine required tags from operation type
|
if not available_models:
|
||||||
requiredTags = options.requiredTags
|
logger.error("No models available in the registry")
|
||||||
if not requiredTags:
|
raise ValueError("No AI models available")
|
||||||
requiredTags = OPERATION_TAG_MAPPING.get(options.operationType, [ModelTags.TEXT, ModelTags.CHAT])
|
|
||||||
|
|
||||||
|
# Use the dynamic model selector
|
||||||
|
selected_model = model_selector.selectModel(prompt, context, options, available_models)
|
||||||
|
|
||||||
# Override priority based on processing mode if not explicitly set
|
if not selected_model:
|
||||||
effectivePriority = options.priority
|
logger.error("No suitable model found for the given criteria")
|
||||||
if options.priority == Priority.BALANCED:
|
raise ValueError("No suitable AI model found")
|
||||||
effectivePriority = PROCESSING_MODE_PRIORITY_MAPPING.get(options.processingMode, Priority.BALANCED)
|
|
||||||
|
|
||||||
logger.info(f"Model selection - Operation: {options.operationType}, Required tags: {requiredTags}, Priority: {effectivePriority}")
|
logger.info(f"Selected model: {selected_model.name} ({selected_model.displayName})")
|
||||||
|
return selected_model.name
|
||||||
|
|
||||||
for name, info in aiModels.items():
|
|
||||||
logger.info(f"Checking model: {name}, tags: {info.get('tags', [])}, function: {info.get('function', 'unknown')}")
|
|
||||||
# Check context length
|
|
||||||
if info["contextLength"] > 0 and totalSize > info["contextLength"] * 0.8:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check cost constraints
|
|
||||||
if options.maxCost is not None:
|
|
||||||
if self._estimateCost(info, totalSize) > options.maxCost:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check required tags/capabilities
|
|
||||||
modelTags = info.get("tags", [])
|
|
||||||
if requiredTags and not all(tag in modelTags for tag in requiredTags):
|
|
||||||
logger.info(f" -> Skipping {name}: missing required tags. Has: {modelTags}, needs: {requiredTags}")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
logger.info(f" -> {name} passed tag check")
|
|
||||||
|
|
||||||
# Check processing mode requirements
|
|
||||||
if options.processingMode == ProcessingMode.DETAILED and ModelTags.FAST in modelTags:
|
|
||||||
# Skip fast models for detailed processing
|
|
||||||
continue
|
|
||||||
|
|
||||||
candidates[name] = info
|
|
||||||
logger.info(f" -> {name} added to candidates")
|
|
||||||
|
|
||||||
logger.info(f"Final candidates: {list(candidates.keys())}")
|
|
||||||
|
|
||||||
if not candidates:
|
|
||||||
logger.info("No candidates found, using fallback")
|
|
||||||
# Fallback based on operation type
|
|
||||||
if options.operationType == OperationType.IMAGE_ANALYSIS:
|
|
||||||
logger.info("Using fallback: openai_callAiImage")
|
|
||||||
return "openai_callAiImage"
|
|
||||||
elif options.operationType == OperationType.IMAGE_GENERATION:
|
|
||||||
logger.info("Using fallback: openai_generateImage")
|
|
||||||
return "openai_generateImage"
|
|
||||||
elif options.operationType == OperationType.WEB_RESEARCH:
|
|
||||||
logger.info("Using fallback: perplexity_callAiWithWebSearch")
|
|
||||||
return "perplexity_callAiWithWebSearch"
|
|
||||||
else:
|
|
||||||
logger.info("Using fallback: openai_callAiBasic_gpt35")
|
|
||||||
return "openai_callAiBasic_gpt35"
|
|
||||||
|
|
||||||
# Special handling for planning operations - use Claude for consistency
|
|
||||||
if options.operationType in [OperationType.GENERATE_PLAN, OperationType.ANALYSE_CONTENT]:
|
|
||||||
if "anthropic_callAiBasic" in candidates:
|
|
||||||
logger.info("Planning operation: Selected Claude (anthropic_callAiBasic) for highest quality")
|
|
||||||
return "anthropic_callAiBasic"
|
|
||||||
|
|
||||||
# Fallback to GPT-4o if Claude not available
|
|
||||||
if "openai_callAiBasic" in candidates:
|
|
||||||
logger.info("Planning operation: Selected GPT-4o (openai_callAiBasic) as fallback")
|
|
||||||
return "openai_callAiBasic"
|
|
||||||
|
|
||||||
# Select based on priority for other operations
|
|
||||||
if effectivePriority == Priority.SPEED:
|
|
||||||
selected = max(candidates, key=lambda k: candidates[k]["speedRating"])
|
|
||||||
logger.info(f"Selected by SPEED: {selected}")
|
|
||||||
return selected
|
|
||||||
elif effectivePriority == Priority.QUALITY:
|
|
||||||
selected = max(candidates, key=lambda k: candidates[k]["qualityRating"])
|
|
||||||
logger.info(f"Selected by QUALITY: {selected}")
|
|
||||||
return selected
|
|
||||||
elif effectivePriority == Priority.COST:
|
|
||||||
selected = min(candidates, key=lambda k: candidates[k]["costPer1kTokens"])
|
|
||||||
logger.info(f"Selected by COST: {selected}")
|
|
||||||
return selected
|
|
||||||
else: # BALANCED
|
|
||||||
def balancedScore(name: str) -> float:
|
|
||||||
info = candidates[name]
|
|
||||||
return info["qualityRating"] * 0.4 + info["speedRating"] * 0.3 + (10 - info["costPer1kTokens"] * 1000) * 0.3
|
|
||||||
|
|
||||||
selected = max(candidates, key=balancedScore)
|
|
||||||
logger.info(f"Selected by BALANCED: {selected}")
|
|
||||||
return selected
|
|
||||||
|
|
||||||
def _getFallbackModels(self, operationType: str) -> List[str]:
|
|
||||||
"""Get ordered list of fallback models for a given operation type."""
|
|
||||||
fallbackMappings = {
|
|
||||||
OperationType.GENERAL: [
|
|
||||||
"openai_callAiBasic_gpt35", # Fast and reliable
|
|
||||||
"openai_callAiBasic", # High quality
|
|
||||||
"anthropic_callAiBasic", # Alternative high quality
|
|
||||||
"perplexity_callAiBasic" # Cost effective
|
|
||||||
],
|
|
||||||
OperationType.IMAGE_ANALYSIS: [
|
|
||||||
"openai_callAiImage", # Primary image analysis
|
|
||||||
"anthropic_callAiImage" # Alternative image analysis
|
|
||||||
],
|
|
||||||
OperationType.IMAGE_GENERATION: [
|
|
||||||
"openai_generateImage" # Only image generation model
|
|
||||||
],
|
|
||||||
OperationType.WEB_RESEARCH: [
|
|
||||||
"perplexity_callAiWithWebSearch", # Primary web research
|
|
||||||
"perplexity_callAiBasic", # Alternative with web search
|
|
||||||
"openai_callAiBasic" # Fallback to general model
|
|
||||||
],
|
|
||||||
OperationType.GENERATE_PLAN: [
|
|
||||||
"anthropic_callAiBasic", # Best for planning
|
|
||||||
"openai_callAiBasic", # High quality alternative
|
|
||||||
"openai_callAiBasic_gpt35" # Fast fallback
|
|
||||||
],
|
|
||||||
OperationType.ANALYSE_CONTENT: [
|
|
||||||
"anthropic_callAiBasic", # Best for analysis
|
|
||||||
"openai_callAiBasic", # High quality alternative
|
|
||||||
"openai_callAiBasic_gpt35" # Fast fallback
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackMappings.get(operationType, fallbackMappings[OperationType.GENERAL])
|
|
||||||
|
|
||||||
def _connectorFor(self, modelName: str):
|
|
||||||
"""Get the appropriate connector for the model."""
|
|
||||||
connectorType = aiModels[modelName]["connector"]
|
|
||||||
if connectorType == "openai":
|
|
||||||
return self.openaiService
|
|
||||||
elif connectorType == "anthropic":
|
|
||||||
return self.anthropicService
|
|
||||||
elif connectorType == "perplexity":
|
|
||||||
return self.perplexityService
|
|
||||||
elif connectorType == "tavily":
|
|
||||||
return self.tavilyService
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown connector type: {connectorType}")
|
|
||||||
|
|
||||||
async def call(self, request: AiCallRequest) -> AiCallResponse:
|
async def call(self, request: AiCallRequest) -> AiCallResponse:
|
||||||
"""Call AI model for text generation with fallback mechanism."""
|
"""Call AI model for text generation using dynamic model selection."""
|
||||||
|
|
||||||
prompt = request.prompt
|
prompt = request.prompt
|
||||||
context = request.context or ""
|
context = request.context or ""
|
||||||
|
|
@ -516,124 +116,101 @@ class AiObjects:
|
||||||
# Our continuation system handles stopping early via prompt engineering
|
# Our continuation system handles stopping early via prompt engineering
|
||||||
|
|
||||||
|
|
||||||
# Get fallback models for this operation type
|
try:
|
||||||
fallbackModels = self._getFallbackModels(options.operationType)
|
# Select the best model using dynamic selection
|
||||||
|
modelName = self._selectModel(prompt, context, options)
|
||||||
|
selectedModel = model_registry.getModel(modelName)
|
||||||
|
|
||||||
# Try primary model first, then fallbacks
|
if not selectedModel:
|
||||||
lastError = None
|
raise ValueError(f"Selected model {modelName} not found in registry")
|
||||||
for attempt, modelName in enumerate(fallbackModels):
|
# Replace <TOKEN_LIMIT> placeholder in prompt for this specific model
|
||||||
try:
|
context_length = selectedModel.contextLength
|
||||||
logger.info(f"Attempting AI call with model: {modelName} (attempt {attempt + 1}/{len(fallbackModels)})")
|
if context_length > 0:
|
||||||
|
token_limit = str(context_length)
|
||||||
|
else:
|
||||||
|
token_limit = "16000" # Default for text generation
|
||||||
|
|
||||||
# Replace <TOKEN_LIMIT> placeholder in prompt for this specific model
|
# Create a copy of the prompt for this model call
|
||||||
context_length = aiModels[modelName].get("contextLength", 0)
|
modelPrompt = prompt
|
||||||
if context_length > 0:
|
if "<TOKEN_LIMIT>" in modelPrompt:
|
||||||
token_limit = str(context_length)
|
modelPrompt = modelPrompt.replace("<TOKEN_LIMIT>", token_limit)
|
||||||
else:
|
logger.debug(f"Replaced <TOKEN_LIMIT> with {token_limit} for model {modelName}")
|
||||||
token_limit = "16000" # Default for text generation
|
|
||||||
|
|
||||||
# Create a copy of the prompt for this model call
|
# Update messages array with replaced content
|
||||||
modelPrompt = prompt
|
messages = []
|
||||||
if "<TOKEN_LIMIT>" in modelPrompt:
|
if context:
|
||||||
modelPrompt = modelPrompt.replace("<TOKEN_LIMIT>", token_limit)
|
messages.append({"role": "system", "content": f"Context from documents:\n{context}"})
|
||||||
logger.debug(f"Replaced <TOKEN_LIMIT> with {token_limit} for model {modelName}")
|
messages.append({"role": "user", "content": modelPrompt})
|
||||||
|
|
||||||
# Update messages array with replaced content
|
# Start timing
|
||||||
messages = []
|
startTime = time.time()
|
||||||
if context:
|
|
||||||
messages.append({"role": "system", "content": f"Context from documents:\n{context}"})
|
|
||||||
messages.append({"role": "user", "content": modelPrompt})
|
|
||||||
|
|
||||||
# Start timing
|
# Get the connector for this model
|
||||||
startTime = time.time()
|
connector = model_registry.getConnectorForModel(modelName)
|
||||||
|
if not connector:
|
||||||
|
raise ValueError(f"No connector found for model {modelName}")
|
||||||
|
|
||||||
connector = self._connectorFor(modelName)
|
# Call the model's function directly
|
||||||
functionName = aiModels[modelName]["function"]
|
if selectedModel.functionCall:
|
||||||
|
# Use the model's function call directly
|
||||||
# Call the appropriate function
|
if modelName.startswith("perplexity_callAiWithWebSearch"):
|
||||||
if functionName == "callAiBasic":
|
|
||||||
if aiModels[modelName]["connector"] == "openai":
|
|
||||||
content = await connector.callAiBasic(messages, temperature=temperature, maxTokens=maxTokens)
|
|
||||||
elif aiModels[modelName]["connector"] == "perplexity":
|
|
||||||
content = await connector.callAiBasic(messages, temperature=temperature, maxTokens=maxTokens)
|
|
||||||
else:
|
|
||||||
response = await connector.callAiBasic(messages, temperature=temperature, maxTokens=maxTokens)
|
|
||||||
content = response["choices"][0]["message"]["content"]
|
|
||||||
elif functionName == "callAiWithWebSearch":
|
|
||||||
# Perplexity web search function
|
|
||||||
query = modelPrompt
|
query = modelPrompt
|
||||||
if context:
|
if context:
|
||||||
query = f"Context: {context}\n\nQuery: {modelPrompt}"
|
query = f"Context: {context}\n\nQuery: {modelPrompt}"
|
||||||
content = await connector.callAiWithWebSearch(query)
|
content = await selectedModel.functionCall(query, temperature=temperature, maxTokens=maxTokens)
|
||||||
elif functionName == "researchTopic":
|
elif modelName.startswith("perplexity_researchTopic"):
|
||||||
# Perplexity research function
|
content = await selectedModel.functionCall(modelPrompt)
|
||||||
content = await connector.researchTopic(modelPrompt)
|
elif modelName.startswith("perplexity_answerQuestion"):
|
||||||
elif functionName == "answerQuestion":
|
content = await selectedModel.functionCall(modelPrompt, context)
|
||||||
# Perplexity question answering function
|
elif modelName.startswith("perplexity_getCurrentNews"):
|
||||||
content = await connector.answerQuestion(modelPrompt, context)
|
content = await selectedModel.functionCall(modelPrompt)
|
||||||
elif functionName == "getCurrentNews":
|
|
||||||
# Perplexity news function
|
|
||||||
content = await connector.getCurrentNews(modelPrompt)
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Function {functionName} not supported for text generation")
|
# Standard callAiBasic
|
||||||
|
if selectedModel.connectorType == "anthropic":
|
||||||
|
response = await selectedModel.functionCall(messages, temperature=temperature, maxTokens=maxTokens)
|
||||||
|
content = response["choices"][0]["message"]["content"]
|
||||||
|
else:
|
||||||
|
content = await selectedModel.functionCall(messages, temperature=temperature, maxTokens=maxTokens)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Model {modelName} has no function call defined")
|
||||||
|
|
||||||
# Calculate timing and output bytes
|
# Calculate timing and output bytes
|
||||||
endTime = time.time()
|
endTime = time.time()
|
||||||
processingTime = endTime - startTime
|
processingTime = endTime - startTime
|
||||||
outputBytes = len(content.encode("utf-8"))
|
outputBytes = len(content.encode("utf-8"))
|
||||||
|
|
||||||
# Calculate price
|
# Calculate price using model's cost information
|
||||||
priceUsd = aiModels[modelName]["calculatePriceUsd"](processingTime, inputBytes, outputBytes)
|
estimated_tokens = inputBytes / 4
|
||||||
|
priceUsd = (estimated_tokens / 1000) * selectedModel.costPer1kTokens + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
|
||||||
|
|
||||||
logger.info(f"✅ AI call successful with model: {modelName}")
|
logger.info(f"✅ AI call successful with model: {modelName}")
|
||||||
return AiCallResponse(
|
logger.info(f" Processing time: {processingTime:.2f}s")
|
||||||
content=content,
|
logger.info(f" Input: {inputBytes} bytes, Output: {outputBytes} bytes")
|
||||||
modelName=modelName,
|
logger.info(f" Estimated cost: ${priceUsd:.4f}")
|
||||||
priceUsd=priceUsd,
|
|
||||||
processingTime=processingTime,
|
|
||||||
bytesSent=inputBytes,
|
|
||||||
bytesReceived=outputBytes,
|
|
||||||
errorCount=0
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
return AiCallResponse(
|
||||||
lastError = e
|
success=True,
|
||||||
# Enhanced error logging with more details
|
content=content,
|
||||||
error_details = str(e)
|
model=modelName,
|
||||||
if hasattr(e, 'detail'):
|
processingTime=processingTime,
|
||||||
error_details = f"{error_details} (detail: {e.detail})"
|
priceUsd=priceUsd,
|
||||||
if hasattr(e, 'status_code'):
|
bytesSent=inputBytes,
|
||||||
error_details = f"{error_details} (status: {e.status_code})"
|
bytesReceived=outputBytes,
|
||||||
|
errorCount=0
|
||||||
|
)
|
||||||
|
|
||||||
logger.warning(f"❌ AI call failed with model {modelName}: {error_details}")
|
except Exception as e:
|
||||||
|
logger.error(f"❌ AI call failed: {e}")
|
||||||
# If this is not the last model, try the next one
|
return AiCallResponse(
|
||||||
if attempt < len(fallbackModels) - 1:
|
success=False,
|
||||||
logger.info(f"🔄 Trying next fallback model...")
|
content=f"AI call failed: {str(e)}",
|
||||||
continue
|
model="none",
|
||||||
else:
|
processingTime=0.0,
|
||||||
# All models failed
|
priceUsd=0.0,
|
||||||
logger.error(f"💥 All {len(fallbackModels)} models failed for operation {options.operationType}")
|
bytesSent=inputBytes,
|
||||||
break
|
bytesReceived=0,
|
||||||
|
errorCount=1
|
||||||
# All fallback attempts failed - return error response
|
)
|
||||||
last_error_details = str(lastError)
|
|
||||||
if hasattr(lastError, 'detail'):
|
|
||||||
last_error_details = f"{last_error_details} (detail: {lastError.detail})"
|
|
||||||
if hasattr(lastError, 'status_code'):
|
|
||||||
last_error_details = f"{last_error_details} (status: {lastError.status_code})"
|
|
||||||
|
|
||||||
errorMsg = f"All AI models failed for operation {options.operationType}. Last error: {last_error_details}"
|
|
||||||
logger.error(errorMsg)
|
|
||||||
return AiCallResponse(
|
|
||||||
content=errorMsg,
|
|
||||||
modelName="error",
|
|
||||||
priceUsd=0.0,
|
|
||||||
processingTime=0.0,
|
|
||||||
bytesSent=inputBytes,
|
|
||||||
bytesReceived=0,
|
|
||||||
errorCount=1
|
|
||||||
)
|
|
||||||
|
|
||||||
async def callImage(self, prompt: str, imageData: Union[str, bytes], mimeType: str = None, options: AiCallOptions = None) -> AiCallResponse:
|
async def callImage(self, prompt: str, imageData: Union[str, bytes], mimeType: str = None, options: AiCallOptions = None) -> AiCallResponse:
|
||||||
"""Call AI model for image analysis with fallback mechanism."""
|
"""Call AI model for image analysis with fallback mechanism."""
|
||||||
|
|
@ -644,70 +221,61 @@ class AiObjects:
|
||||||
# Calculate input bytes (prompt + image data)
|
# Calculate input bytes (prompt + image data)
|
||||||
inputBytes = len(prompt.encode("utf-8")) + len(imageData) if isinstance(imageData, bytes) else len(prompt.encode("utf-8")) + len(str(imageData).encode("utf-8"))
|
inputBytes = len(prompt.encode("utf-8")) + len(imageData) if isinstance(imageData, bytes) else len(prompt.encode("utf-8")) + len(str(imageData).encode("utf-8"))
|
||||||
|
|
||||||
# Get fallback models for image analysis
|
try:
|
||||||
fallbackModels = self._getFallbackModels(OperationType.IMAGE_ANALYSIS)
|
# Select the best model for image analysis
|
||||||
|
modelName = self._selectModel(prompt, "", options)
|
||||||
|
selectedModel = model_registry.getModel(modelName)
|
||||||
|
|
||||||
# Try primary model first, then fallbacks
|
if not selectedModel:
|
||||||
lastError = None
|
raise ValueError(f"Selected model {modelName} not found in registry")
|
||||||
for attempt, modelName in enumerate(fallbackModels):
|
|
||||||
try:
|
|
||||||
logger.info(f"Attempting image analysis with model: {modelName} (attempt {attempt + 1}/{len(fallbackModels)})")
|
|
||||||
|
|
||||||
# Start timing
|
# Get the connector for this model
|
||||||
startTime = time.time()
|
connector = model_registry.getConnectorForModel(modelName)
|
||||||
|
if not connector:
|
||||||
|
raise ValueError(f"No connector found for model {modelName}")
|
||||||
|
|
||||||
connector = self._connectorFor(modelName)
|
# Start timing
|
||||||
functionName = aiModels[modelName]["function"]
|
startTime = time.time()
|
||||||
|
|
||||||
if functionName == "callAiImage":
|
# Call the model's function directly
|
||||||
content = await connector.callAiImage(prompt, imageData, mimeType)
|
if selectedModel.functionCall:
|
||||||
|
content = await selectedModel.functionCall(prompt, imageData, mimeType)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Model {modelName} has no function call defined")
|
||||||
|
|
||||||
# Calculate timing and output bytes
|
# Calculate timing and output bytes
|
||||||
endTime = time.time()
|
endTime = time.time()
|
||||||
processingTime = endTime - startTime
|
processingTime = endTime - startTime
|
||||||
outputBytes = len(content.encode("utf-8"))
|
outputBytes = len(content.encode("utf-8"))
|
||||||
|
|
||||||
# Calculate price
|
# Calculate price using model's cost information
|
||||||
priceUsd = aiModels[modelName]["calculatePriceUsd"](processingTime, inputBytes, outputBytes)
|
estimated_tokens = inputBytes / 4
|
||||||
|
priceUsd = (estimated_tokens / 1000) * selectedModel.costPer1kTokens + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
|
||||||
|
|
||||||
logger.info(f"✅ Image analysis successful with model: {modelName}")
|
logger.info(f"✅ Image analysis successful with model: {modelName}")
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
content=content,
|
success=True,
|
||||||
modelName=modelName,
|
content=content,
|
||||||
priceUsd=priceUsd,
|
model=modelName,
|
||||||
processingTime=processingTime,
|
processingTime=processingTime,
|
||||||
bytesSent=inputBytes,
|
priceUsd=priceUsd,
|
||||||
bytesReceived=outputBytes,
|
bytesSent=inputBytes,
|
||||||
errorCount=0
|
bytesReceived=outputBytes,
|
||||||
)
|
errorCount=0
|
||||||
else:
|
)
|
||||||
raise ValueError(f"Function {functionName} not supported for image analysis")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
lastError = e
|
logger.error(f"❌ Image analysis failed: {e}")
|
||||||
logger.warning(f"❌ Image analysis failed with model {modelName}: {str(e)}")
|
return AiCallResponse(
|
||||||
|
success=False,
|
||||||
# If this is not the last model, try the next one
|
content=f"Image analysis failed: {str(e)}",
|
||||||
if attempt < len(fallbackModels) - 1:
|
model="none",
|
||||||
logger.info(f"🔄 Trying next fallback model for image analysis...")
|
processingTime=0.0,
|
||||||
continue
|
priceUsd=0.0,
|
||||||
else:
|
bytesSent=inputBytes,
|
||||||
# All models failed
|
bytesReceived=0,
|
||||||
logger.error(f"💥 All {len(fallbackModels)} models failed for image analysis")
|
errorCount=1
|
||||||
break
|
)
|
||||||
|
|
||||||
# All fallback attempts failed - return error response
|
|
||||||
errorMsg = f"All AI models failed for image analysis. Last error: {str(lastError)}"
|
|
||||||
logger.error(errorMsg)
|
|
||||||
return AiCallResponse(
|
|
||||||
content=errorMsg,
|
|
||||||
modelName="error",
|
|
||||||
priceUsd=0.0,
|
|
||||||
processingTime=0.0,
|
|
||||||
bytesSent=inputBytes,
|
|
||||||
bytesReceived=0,
|
|
||||||
errorCount=1
|
|
||||||
)
|
|
||||||
|
|
||||||
async def generateImage(self, prompt: str, size: str = "1024x1024", quality: str = "standard", style: str = "vivid", options: AiCallOptions = None) -> AiCallResponse:
|
async def generateImage(self, prompt: str, size: str = "1024x1024", quality: str = "standard", style: str = "vivid", options: AiCallOptions = None) -> AiCallResponse:
|
||||||
"""Generate an image using AI."""
|
"""Generate an image using AI."""
|
||||||
|
|
@ -718,42 +286,45 @@ class AiObjects:
|
||||||
# Calculate input bytes
|
# Calculate input bytes
|
||||||
inputBytes = len(prompt.encode("utf-8"))
|
inputBytes = len(prompt.encode("utf-8"))
|
||||||
|
|
||||||
# Select model for image generation
|
|
||||||
modelName = self._selectModel(prompt, "", options)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Select the best model for image generation
|
||||||
|
modelName = self._selectModel(prompt, "", options)
|
||||||
|
selectedModel = model_registry.getModel(modelName)
|
||||||
|
|
||||||
|
if not selectedModel:
|
||||||
|
raise ValueError(f"Selected model {modelName} not found in registry")
|
||||||
|
|
||||||
|
# Get the connector for this model
|
||||||
|
connector = model_registry.getConnectorForModel(modelName)
|
||||||
|
if not connector:
|
||||||
|
raise ValueError(f"No connector found for model {modelName}")
|
||||||
|
|
||||||
# Start timing
|
# Start timing
|
||||||
startTime = time.time()
|
startTime = time.time()
|
||||||
|
|
||||||
connector = self._connectorFor(modelName)
|
# Call the model's function directly
|
||||||
functionName = aiModels[modelName]["function"]
|
if selectedModel.functionCall:
|
||||||
|
result = await selectedModel.functionCall(prompt, size, quality, style)
|
||||||
if functionName == "generateImage":
|
|
||||||
result = await connector.generateImage(prompt, size, quality, style)
|
|
||||||
content = str(result)
|
content = str(result)
|
||||||
elif functionName == "generateImageWithVariations":
|
|
||||||
results = await connector.generateImageWithVariations(prompt, 1, size, quality, style)
|
|
||||||
result = results[0] if results else {}
|
|
||||||
content = str(result)
|
|
||||||
elif functionName == "generateImageWithChat":
|
|
||||||
content = await connector.generateImageWithChat(prompt, size, quality, style)
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Function {functionName} not supported for image generation")
|
raise ValueError(f"Model {modelName} has no function call defined")
|
||||||
|
|
||||||
# Calculate timing and output bytes
|
# Calculate timing and output bytes
|
||||||
endTime = time.time()
|
endTime = time.time()
|
||||||
processingTime = endTime - startTime
|
processingTime = endTime - startTime
|
||||||
outputBytes = len(content.encode("utf-8"))
|
outputBytes = len(content.encode("utf-8"))
|
||||||
|
|
||||||
# Calculate price
|
# Calculate price using model's cost information
|
||||||
priceUsd = aiModels[modelName]["calculatePriceUsd"](processingTime, inputBytes, outputBytes)
|
estimated_tokens = inputBytes / 4
|
||||||
|
priceUsd = (estimated_tokens / 1000) * selectedModel.costPer1kTokens + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
|
||||||
|
|
||||||
logger.info(f"✅ Image generation successful with model: {modelName}")
|
logger.info(f"✅ Image generation successful with model: {modelName}")
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
|
success=True,
|
||||||
content=content,
|
content=content,
|
||||||
modelName=modelName,
|
model=modelName,
|
||||||
priceUsd=priceUsd,
|
|
||||||
processingTime=processingTime,
|
processingTime=processingTime,
|
||||||
|
priceUsd=priceUsd,
|
||||||
bytesSent=inputBytes,
|
bytesSent=inputBytes,
|
||||||
bytesReceived=outputBytes,
|
bytesReceived=outputBytes,
|
||||||
errorCount=0
|
errorCount=0
|
||||||
|
|
@ -779,7 +350,11 @@ class AiObjects:
|
||||||
max_results=max_results,
|
max_results=max_results,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
result = await self.tavilyService.search(request)
|
# Get Tavily connector from registry
|
||||||
|
tavily_connector = model_registry.getConnectorForModel("tavily_search")
|
||||||
|
if not tavily_connector:
|
||||||
|
raise ValueError("Tavily connector not available")
|
||||||
|
result = await tavily_connector.search(request)
|
||||||
|
|
||||||
if result.success and result.documents:
|
if result.success and result.documents:
|
||||||
return result.documents[0].documentData.results
|
return result.documents[0].documentData.results
|
||||||
|
|
@ -814,7 +389,11 @@ class AiObjects:
|
||||||
extract_depth=extract_depth,
|
extract_depth=extract_depth,
|
||||||
format=format
|
format=format
|
||||||
)
|
)
|
||||||
result = await self.tavilyService.crawl(request)
|
# Get Tavily connector from registry
|
||||||
|
tavily_connector = model_registry.getConnectorForModel("tavily_crawl")
|
||||||
|
if not tavily_connector:
|
||||||
|
raise ValueError("Tavily connector not available")
|
||||||
|
result = await tavily_connector.crawl(request)
|
||||||
|
|
||||||
if result.success and result.documents:
|
if result.success and result.documents:
|
||||||
return result.documents[0].documentData.results
|
return result.documents[0].documentData.results
|
||||||
|
|
@ -1163,15 +742,23 @@ Format your response in a clear, professional manner that would be helpful for s
|
||||||
startTime = time.time()
|
startTime = time.time()
|
||||||
|
|
||||||
# Use Perplexity for web research with search capabilities
|
# Use Perplexity for web research with search capabilities
|
||||||
response = await self.perplexityService.callAiWithWebSearch(webPrompt)
|
perplexity_connector = model_registry.getConnectorForModel("perplexity_callAiWithWebSearch")
|
||||||
|
if not perplexity_connector:
|
||||||
|
raise ValueError("Perplexity connector not available")
|
||||||
|
response = await perplexity_connector.callAiWithWebSearch(webPrompt)
|
||||||
|
|
||||||
# Calculate timing and output bytes
|
# Calculate timing and output bytes
|
||||||
endTime = time.time()
|
endTime = time.time()
|
||||||
processingTime = endTime - startTime
|
processingTime = endTime - startTime
|
||||||
outputBytes = len(response.encode("utf-8"))
|
outputBytes = len(response.encode("utf-8"))
|
||||||
|
|
||||||
# Calculate price (use perplexity model pricing)
|
# Calculate price using Perplexity model pricing
|
||||||
priceUsd = aiModels["perplexity_callAiWithWebSearch"]["calculatePriceUsd"](processingTime, inputBytes, outputBytes)
|
perplexity_model = model_registry.getModel("perplexity_callAiWithWebSearch")
|
||||||
|
if perplexity_model:
|
||||||
|
estimated_tokens = inputBytes / 4
|
||||||
|
priceUsd = (estimated_tokens / 1000) * perplexity_model.costPer1kTokens + (outputBytes / 4 / 1000) * perplexity_model.costPer1kTokensOutput
|
||||||
|
else:
|
||||||
|
priceUsd = 0.0
|
||||||
|
|
||||||
logger.info(f"✅ Web query successful with Perplexity")
|
logger.info(f"✅ Web query successful with Perplexity")
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
|
|
@ -1198,23 +785,27 @@ Format your response in a clear, professional manner that would be helpful for s
|
||||||
# Utility methods
|
# Utility methods
|
||||||
async def listAvailableModels(self, connectorType: str = None) -> List[Dict[str, Any]]:
|
async def listAvailableModels(self, connectorType: str = None) -> List[Dict[str, Any]]:
|
||||||
"""List available models, optionally filtered by connector type."""
|
"""List available models, optionally filtered by connector type."""
|
||||||
|
models = model_registry.getAvailableModels()
|
||||||
if connectorType:
|
if connectorType:
|
||||||
return [info for name, info in aiModels.items() if info["connector"] == connectorType]
|
return [model.dict() for model in models if model.connectorType == connectorType]
|
||||||
return list(aiModels.values())
|
return [model.dict() for model in models]
|
||||||
|
|
||||||
async def getModelInfo(self, modelName: str) -> Dict[str, Any]:
|
async def getModelInfo(self, modelName: str) -> Dict[str, Any]:
|
||||||
"""Get information about a specific model."""
|
"""Get information about a specific model."""
|
||||||
if modelName not in aiModels:
|
model = model_registry.getModel(modelName)
|
||||||
|
if not model:
|
||||||
raise ValueError(f"Model {modelName} not found")
|
raise ValueError(f"Model {modelName} not found")
|
||||||
return aiModels[modelName]
|
return model.dict()
|
||||||
|
|
||||||
async def getModelsByCapability(self, capability: str) -> List[str]:
|
async def getModelsByCapability(self, capability: str) -> List[str]:
|
||||||
"""Get model names that support a specific capability."""
|
"""Get model names that support a specific capability."""
|
||||||
return [name for name, info in aiModels.items() if capability in info.get("capabilities", [])]
|
models = model_registry.getModelsByCapability(capability)
|
||||||
|
return [model.name for model in models]
|
||||||
|
|
||||||
async def getModelsByTag(self, tag: str) -> List[str]:
|
async def getModelsByTag(self, tag: str) -> List[str]:
|
||||||
"""Get model names that have a specific tag."""
|
"""Get model names that have a specific tag."""
|
||||||
return [name for name, info in aiModels.items() if tag in info.get("tags", [])]
|
models = model_registry.getModelsByTag(tag)
|
||||||
|
return [model.name for model in models]
|
||||||
|
|
||||||
async def selectRelevantWebsites(self, websites: List[str], userQuestion: str) -> Tuple[List[str], str]:
|
async def selectRelevantWebsites(self, websites: List[str], userQuestion: str) -> Tuple[List[str], str]:
|
||||||
"""Select most relevant websites using AI analysis. Returns (selected_websites, ai_response)."""
|
"""Select most relevant websites using AI analysis. Returns (selected_websites, ai_response)."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue