refactored ai core system to attach ai models plug and play

This commit is contained in:
ValueOn AG 2025-10-21 18:14:58 +02:00
parent 52adedab4a
commit 3adaaad8eb
11 changed files with 2721 additions and 688 deletions

View 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]

View 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()

View 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()

View 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()

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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):

View file

@ -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."""

File diff suppressed because it is too large Load diff

View file

@ -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)."""