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 fastapi import HTTPException
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -17,10 +19,11 @@ def loadConfigData():
|
|||
"temperature": float(APP_CONFIG.get('Connector_AiAnthropic_TEMPERATURE')),
|
||||
}
|
||||
|
||||
class AiAnthropic:
|
||||
class AiAnthropic(BaseConnectorAi):
|
||||
"""Connector for communication with the Anthropic API."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Load configuration
|
||||
self.config = loadConfigData()
|
||||
self.apiKey = self.config["apiKey"]
|
||||
|
|
@ -39,25 +42,51 @@ class AiAnthropic:
|
|||
|
||||
logger.info(f"Anthropic Connector initialized with model: {self.modelName}")
|
||||
|
||||
def _getMaxTokensForModel(self, maxTokens: int = None) -> int:
|
||||
"""Get appropriate max_tokens for the current model."""
|
||||
if maxTokens is not None:
|
||||
return maxTokens
|
||||
|
||||
# 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
|
||||
def getConnectorType(self) -> str:
|
||||
"""Get the connector type identifier."""
|
||||
return "anthropic"
|
||||
|
||||
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"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
# Anthropic requires max_tokens - use model-appropriate value
|
||||
payload["max_tokens"] = self._getMaxTokensForModel(maxTokens)
|
||||
# Anthropic requires max_tokens - use provided value or throw error
|
||||
if maxTokens is None:
|
||||
raise ValueError("maxTokens must be provided for Anthropic API calls")
|
||||
payload["max_tokens"] = maxTokens
|
||||
if system_prompt:
|
||||
payload["system"] = system_prompt
|
||||
|
||||
|
|
@ -4,6 +4,8 @@ import httpx
|
|||
from typing import Dict, Any, List, Union
|
||||
from fastapi import HTTPException
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -21,10 +23,11 @@ def loadConfigData():
|
|||
"temperature": float(APP_CONFIG.get('Connector_AiOpenai_TEMPERATURE')),
|
||||
}
|
||||
|
||||
class AiOpenai:
|
||||
class AiOpenai(BaseConnectorAi):
|
||||
"""Connector for communication with the OpenAI API."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Load configuration
|
||||
self.config = loadConfigData()
|
||||
self.apiKey = self.config["apiKey"]
|
||||
|
|
@ -41,6 +44,87 @@ class AiOpenai:
|
|||
)
|
||||
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:
|
||||
"""
|
||||
Calls the OpenAI API with the given messages.
|
||||
|
|
@ -70,9 +154,10 @@ class AiOpenai:
|
|||
"temperature": temperature
|
||||
}
|
||||
|
||||
# Only add max_tokens if it's explicitly set
|
||||
if maxTokens is not None:
|
||||
payload["max_tokens"] = maxTokens
|
||||
# Add max_tokens - use provided value or throw error
|
||||
if maxTokens is None:
|
||||
raise ValueError("maxTokens must be provided for OpenAI API calls")
|
||||
payload["max_tokens"] = maxTokens
|
||||
|
||||
response = await self.httpClient.post(
|
||||
self.apiUrl,
|
||||
|
|
@ -4,6 +4,8 @@ import asyncio
|
|||
from typing import Dict, Any, List, Union, Optional
|
||||
from fastapi import HTTPException
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -17,10 +19,11 @@ def loadConfigData():
|
|||
"temperature": float(APP_CONFIG.get('Connector_AiPerplexity_TEMPERATURE')),
|
||||
}
|
||||
|
||||
class AiPerplexity:
|
||||
class AiPerplexity(BaseConnectorAi):
|
||||
"""Connector for communication with the Perplexity API."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Load configuration
|
||||
self.config = loadConfigData()
|
||||
self.apiKey = self.config["apiKey"]
|
||||
|
|
@ -39,6 +42,105 @@ class AiPerplexity:
|
|||
|
||||
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:
|
||||
"""
|
||||
Calls the Perplexity API with the given messages.
|
||||
|
|
@ -68,8 +170,10 @@ class AiPerplexity:
|
|||
"temperature": temperature
|
||||
}
|
||||
|
||||
# Add max_tokens - use provided value or default to 128000 (Perplexity's typical limit)
|
||||
payload["max_tokens"] = maxTokens if maxTokens is not None else 128000
|
||||
# Add max_tokens - use provided value or throw error
|
||||
if maxTokens is None:
|
||||
raise ValueError("maxTokens must be provided for Perplexity API calls")
|
||||
payload["max_tokens"] = maxTokens
|
||||
|
||||
response = await self.httpClient.post(
|
||||
self.apiUrl,
|
||||
|
|
@ -134,8 +238,10 @@ class AiPerplexity:
|
|||
"temperature": temperature
|
||||
}
|
||||
|
||||
# Add max_tokens - use provided value or default to 128000 (Perplexity's typical limit)
|
||||
payload["max_tokens"] = maxTokens if maxTokens is not None else 128000
|
||||
# Add max_tokens - use provided value or throw error
|
||||
if maxTokens is None:
|
||||
raise ValueError("maxTokens must be provided for Perplexity API calls")
|
||||
payload["max_tokens"] = maxTokens
|
||||
|
||||
response = await self.httpClient.post(
|
||||
self.apiUrl,
|
||||
|
|
@ -4,10 +4,12 @@
|
|||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
from tavily import AsyncTavilyClient
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
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 (
|
||||
WebSearchActionResult,
|
||||
WebSearchActionDocument,
|
||||
|
|
@ -37,16 +39,100 @@ class WebCrawlResult:
|
|||
url: str
|
||||
content: str
|
||||
|
||||
@dataclass
|
||||
class ConnectorWeb:
|
||||
client: AsyncTavilyClient = None
|
||||
# Cached settings loaded at initialization time
|
||||
crawl_timeout: int = 30
|
||||
crawl_max_retries: int = 3
|
||||
crawl_retry_delay: int = 2
|
||||
# Cached web search constraints (camelCase per project style)
|
||||
webSearchMinResults: int = 1
|
||||
webSearchMaxResults: int = 20
|
||||
class ConnectorWeb(BaseConnectorAi):
|
||||
"""Tavily web search connector."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.client: Optional[AsyncTavilyClient] = None
|
||||
# Cached settings loaded at initialization time
|
||||
self.crawl_timeout: int = 30
|
||||
self.crawl_max_retries: int = 3
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -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):
|
||||
"""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__)
|
||||
|
||||
from modules.connectors.connectorAiOpenai import AiOpenai
|
||||
from modules.connectors.connectorAiAnthropic import AiAnthropic
|
||||
from modules.connectors.connectorAiPerplexity import AiPerplexity
|
||||
from modules.connectors.connectorAiTavily import ConnectorWeb
|
||||
# No more hardcoded imports - everything is discovered dynamically
|
||||
from modules.aicore.aicoreModelRegistry import model_registry
|
||||
from modules.aicore.aicoreModelSelector import model_selector
|
||||
from modules.datamodels.datamodelAi import (
|
||||
AiCallOptions,
|
||||
AiCallRequest,
|
||||
|
|
@ -32,461 +31,62 @@ from modules.datamodels.datamodelWeb import (
|
|||
from modules.datamodels.datamodelChat import ActionDocument
|
||||
|
||||
|
||||
# Comprehensive model registry with capability tags and function mapping
|
||||
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
|
||||
}
|
||||
}
|
||||
# Dynamic model registry - models are now loaded from connectors via aicore system
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AiObjects:
|
||||
"""Centralized AI interface: selects model and calls connector. Includes web functionality."""
|
||||
|
||||
openaiService: AiOpenai
|
||||
anthropicService: AiAnthropic
|
||||
perplexityService: AiPerplexity
|
||||
tavilyService: ConnectorWeb
|
||||
"""Centralized AI interface: dynamically discovers and uses AI models. Includes web functionality."""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.openaiService is None:
|
||||
raise TypeError("openaiService must be provided")
|
||||
if self.anthropicService is None:
|
||||
raise TypeError("anthropicService must be provided")
|
||||
if self.perplexityService is None:
|
||||
raise TypeError("perplexityService must be provided")
|
||||
if self.tavilyService is None:
|
||||
raise TypeError("tavilyService must be provided")
|
||||
# Auto-discover and register all available connectors
|
||||
self._discoverAndRegisterConnectors()
|
||||
|
||||
def _discoverAndRegisterConnectors(self):
|
||||
"""Auto-discover and register all available AI connectors."""
|
||||
logger.info("Auto-discovering AI connectors...")
|
||||
|
||||
# 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
|
||||
async def create(cls) -> "AiObjects":
|
||||
"""Create AiObjects instance with all connectors initialized."""
|
||||
openaiService = AiOpenai()
|
||||
anthropicService = AiAnthropic()
|
||||
perplexityService = AiPerplexity()
|
||||
tavilyService = await ConnectorWeb.create()
|
||||
|
||||
return cls(
|
||||
openaiService=openaiService,
|
||||
anthropicService=anthropicService,
|
||||
perplexityService=perplexityService,
|
||||
tavilyService=tavilyService
|
||||
)
|
||||
"""Create AiObjects instance with auto-discovered connectors."""
|
||||
# No need to manually create connectors - they're auto-discovered
|
||||
return cls()
|
||||
|
||||
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:
|
||||
"""Select the best model based on operation type, tags, and requirements."""
|
||||
totalSize = len(prompt.encode("utf-8")) + len(context.encode("utf-8"))
|
||||
candidates: Dict[str, Dict[str, Any]] = {}
|
||||
"""Select the best model using dynamic model selection system."""
|
||||
# Get available models from the dynamic registry
|
||||
available_models = model_registry.getAvailableModels()
|
||||
|
||||
# Determine required tags from operation type
|
||||
requiredTags = options.requiredTags
|
||||
if not requiredTags:
|
||||
requiredTags = OPERATION_TAG_MAPPING.get(options.operationType, [ModelTags.TEXT, ModelTags.CHAT])
|
||||
if not available_models:
|
||||
logger.error("No models available in the registry")
|
||||
raise ValueError("No AI models available")
|
||||
|
||||
# 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
|
||||
effectivePriority = options.priority
|
||||
if options.priority == Priority.BALANCED:
|
||||
effectivePriority = PROCESSING_MODE_PRIORITY_MAPPING.get(options.processingMode, Priority.BALANCED)
|
||||
if not selected_model:
|
||||
logger.error("No suitable model found for the given criteria")
|
||||
raise ValueError("No suitable AI model found")
|
||||
|
||||
logger.info(f"Model selection - Operation: {options.operationType}, Required tags: {requiredTags}, Priority: {effectivePriority}")
|
||||
|
||||
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
|
||||
logger.info(f"Selected model: {selected_model.name} ({selected_model.displayName})")
|
||||
return selected_model.name
|
||||
|
||||
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:
|
||||
"""Call AI model for text generation with fallback mechanism."""
|
||||
"""Call AI model for text generation using dynamic model selection."""
|
||||
|
||||
prompt = request.prompt
|
||||
context = request.context or ""
|
||||
|
|
@ -516,124 +116,101 @@ class AiObjects:
|
|||
# Our continuation system handles stopping early via prompt engineering
|
||||
|
||||
|
||||
# Get fallback models for this operation type
|
||||
fallbackModels = self._getFallbackModels(options.operationType)
|
||||
|
||||
# Try primary model first, then fallbacks
|
||||
lastError = None
|
||||
for attempt, modelName in enumerate(fallbackModels):
|
||||
try:
|
||||
logger.info(f"Attempting AI call with model: {modelName} (attempt {attempt + 1}/{len(fallbackModels)})")
|
||||
|
||||
# Replace <TOKEN_LIMIT> placeholder in prompt for this specific model
|
||||
context_length = aiModels[modelName].get("contextLength", 0)
|
||||
if context_length > 0:
|
||||
token_limit = str(context_length)
|
||||
else:
|
||||
token_limit = "16000" # Default for text generation
|
||||
|
||||
# Create a copy of the prompt for this model call
|
||||
modelPrompt = prompt
|
||||
if "<TOKEN_LIMIT>" in modelPrompt:
|
||||
modelPrompt = modelPrompt.replace("<TOKEN_LIMIT>", token_limit)
|
||||
logger.debug(f"Replaced <TOKEN_LIMIT> with {token_limit} for model {modelName}")
|
||||
|
||||
# Update messages array with replaced content
|
||||
messages = []
|
||||
if context:
|
||||
messages.append({"role": "system", "content": f"Context from documents:\n{context}"})
|
||||
messages.append({"role": "user", "content": modelPrompt})
|
||||
|
||||
# Start timing
|
||||
startTime = time.time()
|
||||
|
||||
connector = self._connectorFor(modelName)
|
||||
functionName = aiModels[modelName]["function"]
|
||||
|
||||
# Call the appropriate function
|
||||
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
|
||||
try:
|
||||
# Select the best model using dynamic selection
|
||||
modelName = self._selectModel(prompt, context, options)
|
||||
selectedModel = model_registry.getModel(modelName)
|
||||
|
||||
if not selectedModel:
|
||||
raise ValueError(f"Selected model {modelName} not found in registry")
|
||||
# Replace <TOKEN_LIMIT> placeholder in prompt for this specific model
|
||||
context_length = selectedModel.contextLength
|
||||
if context_length > 0:
|
||||
token_limit = str(context_length)
|
||||
else:
|
||||
token_limit = "16000" # Default for text generation
|
||||
|
||||
# Create a copy of the prompt for this model call
|
||||
modelPrompt = prompt
|
||||
if "<TOKEN_LIMIT>" in modelPrompt:
|
||||
modelPrompt = modelPrompt.replace("<TOKEN_LIMIT>", token_limit)
|
||||
logger.debug(f"Replaced <TOKEN_LIMIT> with {token_limit} for model {modelName}")
|
||||
|
||||
# Update messages array with replaced content
|
||||
messages = []
|
||||
if context:
|
||||
messages.append({"role": "system", "content": f"Context from documents:\n{context}"})
|
||||
messages.append({"role": "user", "content": modelPrompt})
|
||||
|
||||
# Start timing
|
||||
startTime = time.time()
|
||||
|
||||
# Get the connector for this model
|
||||
connector = model_registry.getConnectorForModel(modelName)
|
||||
if not connector:
|
||||
raise ValueError(f"No connector found for model {modelName}")
|
||||
|
||||
# Call the model's function directly
|
||||
if selectedModel.functionCall:
|
||||
# Use the model's function call directly
|
||||
if modelName.startswith("perplexity_callAiWithWebSearch"):
|
||||
query = modelPrompt
|
||||
if context:
|
||||
query = f"Context: {context}\n\nQuery: {modelPrompt}"
|
||||
content = await connector.callAiWithWebSearch(query)
|
||||
elif functionName == "researchTopic":
|
||||
# Perplexity research function
|
||||
content = await connector.researchTopic(modelPrompt)
|
||||
elif functionName == "answerQuestion":
|
||||
# Perplexity question answering function
|
||||
content = await connector.answerQuestion(modelPrompt, context)
|
||||
elif functionName == "getCurrentNews":
|
||||
# Perplexity news function
|
||||
content = await connector.getCurrentNews(modelPrompt)
|
||||
content = await selectedModel.functionCall(query, temperature=temperature, maxTokens=maxTokens)
|
||||
elif modelName.startswith("perplexity_researchTopic"):
|
||||
content = await selectedModel.functionCall(modelPrompt)
|
||||
elif modelName.startswith("perplexity_answerQuestion"):
|
||||
content = await selectedModel.functionCall(modelPrompt, context)
|
||||
elif modelName.startswith("perplexity_getCurrentNews"):
|
||||
content = await selectedModel.functionCall(modelPrompt)
|
||||
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
|
||||
endTime = time.time()
|
||||
processingTime = endTime - startTime
|
||||
outputBytes = len(content.encode("utf-8"))
|
||||
|
||||
# Calculate price
|
||||
priceUsd = aiModels[modelName]["calculatePriceUsd"](processingTime, inputBytes, outputBytes)
|
||||
|
||||
logger.info(f"✅ AI call successful with model: {modelName}")
|
||||
return AiCallResponse(
|
||||
content=content,
|
||||
modelName=modelName,
|
||||
priceUsd=priceUsd,
|
||||
processingTime=processingTime,
|
||||
bytesSent=inputBytes,
|
||||
bytesReceived=outputBytes,
|
||||
errorCount=0
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
lastError = e
|
||||
# Enhanced error logging with more details
|
||||
error_details = str(e)
|
||||
if hasattr(e, 'detail'):
|
||||
error_details = f"{error_details} (detail: {e.detail})"
|
||||
if hasattr(e, 'status_code'):
|
||||
error_details = f"{error_details} (status: {e.status_code})"
|
||||
|
||||
logger.warning(f"❌ AI call failed with model {modelName}: {error_details}")
|
||||
|
||||
# If this is not the last model, try the next one
|
||||
if attempt < len(fallbackModels) - 1:
|
||||
logger.info(f"🔄 Trying next fallback model...")
|
||||
continue
|
||||
else:
|
||||
# All models failed
|
||||
logger.error(f"💥 All {len(fallbackModels)} models failed for operation {options.operationType}")
|
||||
break
|
||||
|
||||
# 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})"
|
||||
# Calculate timing and output bytes
|
||||
endTime = time.time()
|
||||
processingTime = endTime - startTime
|
||||
outputBytes = len(content.encode("utf-8"))
|
||||
|
||||
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
|
||||
)
|
||||
# Calculate price using model's cost information
|
||||
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" Processing time: {processingTime:.2f}s")
|
||||
logger.info(f" Input: {inputBytes} bytes, Output: {outputBytes} bytes")
|
||||
logger.info(f" Estimated cost: ${priceUsd:.4f}")
|
||||
|
||||
return AiCallResponse(
|
||||
success=True,
|
||||
content=content,
|
||||
model=modelName,
|
||||
processingTime=processingTime,
|
||||
priceUsd=priceUsd,
|
||||
bytesSent=inputBytes,
|
||||
bytesReceived=outputBytes,
|
||||
errorCount=0
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI call failed: {e}")
|
||||
return AiCallResponse(
|
||||
success=False,
|
||||
content=f"AI call failed: {str(e)}",
|
||||
model="none",
|
||||
processingTime=0.0,
|
||||
priceUsd=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:
|
||||
"""Call AI model for image analysis with fallback mechanism."""
|
||||
|
|
@ -644,70 +221,61 @@ class AiObjects:
|
|||
# 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"))
|
||||
|
||||
# Get fallback models for image analysis
|
||||
fallbackModels = self._getFallbackModels(OperationType.IMAGE_ANALYSIS)
|
||||
|
||||
# Try primary model first, then fallbacks
|
||||
lastError = None
|
||||
for attempt, modelName in enumerate(fallbackModels):
|
||||
try:
|
||||
logger.info(f"Attempting image analysis with model: {modelName} (attempt {attempt + 1}/{len(fallbackModels)})")
|
||||
|
||||
# Start timing
|
||||
startTime = time.time()
|
||||
|
||||
connector = self._connectorFor(modelName)
|
||||
functionName = aiModels[modelName]["function"]
|
||||
|
||||
if functionName == "callAiImage":
|
||||
content = await connector.callAiImage(prompt, imageData, mimeType)
|
||||
|
||||
# Calculate timing and output bytes
|
||||
endTime = time.time()
|
||||
processingTime = endTime - startTime
|
||||
outputBytes = len(content.encode("utf-8"))
|
||||
|
||||
# Calculate price
|
||||
priceUsd = aiModels[modelName]["calculatePriceUsd"](processingTime, inputBytes, outputBytes)
|
||||
|
||||
logger.info(f"✅ Image analysis successful with model: {modelName}")
|
||||
return AiCallResponse(
|
||||
content=content,
|
||||
modelName=modelName,
|
||||
priceUsd=priceUsd,
|
||||
processingTime=processingTime,
|
||||
bytesSent=inputBytes,
|
||||
bytesReceived=outputBytes,
|
||||
errorCount=0
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Function {functionName} not supported for image analysis")
|
||||
|
||||
except Exception as e:
|
||||
lastError = e
|
||||
logger.warning(f"❌ Image analysis failed with model {modelName}: {str(e)}")
|
||||
|
||||
# If this is not the last model, try the next one
|
||||
if attempt < len(fallbackModels) - 1:
|
||||
logger.info(f"🔄 Trying next fallback model for image analysis...")
|
||||
continue
|
||||
else:
|
||||
# All models failed
|
||||
logger.error(f"💥 All {len(fallbackModels)} models failed for image analysis")
|
||||
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
|
||||
)
|
||||
try:
|
||||
# Select the best model for image analysis
|
||||
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
|
||||
startTime = time.time()
|
||||
|
||||
# Call the model's function directly
|
||||
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
|
||||
endTime = time.time()
|
||||
processingTime = endTime - startTime
|
||||
outputBytes = len(content.encode("utf-8"))
|
||||
|
||||
# Calculate price using model's cost information
|
||||
estimated_tokens = inputBytes / 4
|
||||
priceUsd = (estimated_tokens / 1000) * selectedModel.costPer1kTokens + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
|
||||
|
||||
logger.info(f"✅ Image analysis successful with model: {modelName}")
|
||||
return AiCallResponse(
|
||||
success=True,
|
||||
content=content,
|
||||
model=modelName,
|
||||
processingTime=processingTime,
|
||||
priceUsd=priceUsd,
|
||||
bytesSent=inputBytes,
|
||||
bytesReceived=outputBytes,
|
||||
errorCount=0
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Image analysis failed: {e}")
|
||||
return AiCallResponse(
|
||||
success=False,
|
||||
content=f"Image analysis failed: {str(e)}",
|
||||
model="none",
|
||||
processingTime=0.0,
|
||||
priceUsd=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:
|
||||
"""Generate an image using AI."""
|
||||
|
|
@ -718,42 +286,45 @@ class AiObjects:
|
|||
# Calculate input bytes
|
||||
inputBytes = len(prompt.encode("utf-8"))
|
||||
|
||||
# Select model for image generation
|
||||
modelName = self._selectModel(prompt, "", options)
|
||||
|
||||
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
|
||||
startTime = time.time()
|
||||
|
||||
connector = self._connectorFor(modelName)
|
||||
functionName = aiModels[modelName]["function"]
|
||||
|
||||
if functionName == "generateImage":
|
||||
result = await connector.generateImage(prompt, size, quality, style)
|
||||
# Call the model's function directly
|
||||
if selectedModel.functionCall:
|
||||
result = await selectedModel.functionCall(prompt, size, quality, style)
|
||||
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:
|
||||
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
|
||||
endTime = time.time()
|
||||
processingTime = endTime - startTime
|
||||
outputBytes = len(content.encode("utf-8"))
|
||||
|
||||
# Calculate price
|
||||
priceUsd = aiModels[modelName]["calculatePriceUsd"](processingTime, inputBytes, outputBytes)
|
||||
# Calculate price using model's cost information
|
||||
estimated_tokens = inputBytes / 4
|
||||
priceUsd = (estimated_tokens / 1000) * selectedModel.costPer1kTokens + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
|
||||
|
||||
logger.info(f"✅ Image generation successful with model: {modelName}")
|
||||
return AiCallResponse(
|
||||
success=True,
|
||||
content=content,
|
||||
modelName=modelName,
|
||||
priceUsd=priceUsd,
|
||||
model=modelName,
|
||||
processingTime=processingTime,
|
||||
priceUsd=priceUsd,
|
||||
bytesSent=inputBytes,
|
||||
bytesReceived=outputBytes,
|
||||
errorCount=0
|
||||
|
|
@ -779,7 +350,11 @@ class AiObjects:
|
|||
max_results=max_results,
|
||||
**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:
|
||||
return result.documents[0].documentData.results
|
||||
|
|
@ -814,7 +389,11 @@ class AiObjects:
|
|||
extract_depth=extract_depth,
|
||||
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:
|
||||
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()
|
||||
|
||||
# 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
|
||||
endTime = time.time()
|
||||
processingTime = endTime - startTime
|
||||
outputBytes = len(response.encode("utf-8"))
|
||||
|
||||
# Calculate price (use perplexity model pricing)
|
||||
priceUsd = aiModels["perplexity_callAiWithWebSearch"]["calculatePriceUsd"](processingTime, inputBytes, outputBytes)
|
||||
# Calculate price using Perplexity model pricing
|
||||
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")
|
||||
return AiCallResponse(
|
||||
|
|
@ -1198,23 +785,27 @@ Format your response in a clear, professional manner that would be helpful for s
|
|||
# Utility methods
|
||||
async def listAvailableModels(self, connectorType: str = None) -> List[Dict[str, Any]]:
|
||||
"""List available models, optionally filtered by connector type."""
|
||||
models = model_registry.getAvailableModels()
|
||||
if connectorType:
|
||||
return [info for name, info in aiModels.items() if info["connector"] == connectorType]
|
||||
return list(aiModels.values())
|
||||
return [model.dict() for model in models if model.connectorType == connectorType]
|
||||
return [model.dict() for model in models]
|
||||
|
||||
async def getModelInfo(self, modelName: str) -> Dict[str, Any]:
|
||||
"""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")
|
||||
return aiModels[modelName]
|
||||
return model.dict()
|
||||
|
||||
async def getModelsByCapability(self, capability: str) -> List[str]:
|
||||
"""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]:
|
||||
"""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]:
|
||||
"""Select most relevant websites using AI analysis. Returns (selected_websites, ai_response)."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue