implemented dynmaic ai integration and selection chain

This commit is contained in:
ValueOn AG 2025-10-22 01:15:21 +02:00
parent 3adaaad8eb
commit 109e77fd60
34 changed files with 1331 additions and 2316 deletions

View file

@ -71,10 +71,10 @@ class BaseConnectorAi(ABC):
models = self.getCachedModels() models = self.getCachedModels()
return [model for model in models if capability in model.capabilities] return [model for model in models if capability in model.capabilities]
def getModelsByTag(self, tag: str) -> List[AiModel]: def getModelsByPriority(self, priority: str) -> List[AiModel]:
"""Get models that have a specific tag.""" """Get models that have a specific priority."""
models = self.getCachedModels() models = self.getCachedModels()
return [model for model in models if tag in model.tags] return [model for model in models if model.priority == priority]
def getAvailableModels(self) -> List[AiModel]: def getAvailableModels(self) -> List[AiModel]:
"""Get only available models.""" """Get only available models."""

View file

@ -19,40 +19,40 @@ class ModelRegistry:
def __init__(self): def __init__(self):
self._models: Dict[str, AiModel] = {} self._models: Dict[str, AiModel] = {}
self._connectors: Dict[str, BaseConnectorAi] = {} self._connectors: Dict[str, BaseConnectorAi] = {}
self._last_refresh: Optional[float] = None self._lastRefresh: Optional[float] = None
self._refresh_interval: float = 300.0 # 5 minutes self._refreshInterval: float = 300.0 # 5 minutes
def registerConnector(self, connector: BaseConnectorAi): def registerConnector(self, connector: BaseConnectorAi):
"""Register a connector and collect its models.""" """Register a connector and collect its models."""
connector_type = connector.getConnectorType() connectorType = connector.getConnectorType()
self._connectors[connector_type] = connector self._connectors[connectorType] = connector
# Collect models from this connector # Collect models from this connector
try: try:
models = connector.getCachedModels() models = connector.getCachedModels()
for model in models: for model in models:
self._models[model.name] = model self._models[model.name] = model
logger.debug(f"Registered model: {model.name} from {connector_type}") logger.debug(f"Registered model: {model.name} from {connectorType}")
except Exception as e: except Exception as e:
logger.error(f"Failed to register models from {connector_type}: {e}") logger.error(f"Failed to register models from {connectorType}: {e}")
def discoverConnectors(self) -> List[BaseConnectorAi]: def discoverConnectors(self) -> List[BaseConnectorAi]:
"""Auto-discover connectors by scanning aicorePlugin*.py files.""" """Auto-discover connectors by scanning aicorePlugin*.py files."""
connectors = [] connectors = []
connector_dir = os.path.dirname(__file__) connectorDir = os.path.dirname(__file__)
# Scan for connector files # Scan for connector files
for filename in os.listdir(connector_dir): for filename in os.listdir(connectorDir):
if filename.startswith('aicorePlugin') and filename.endswith('.py'): if filename.startswith('aicorePlugin') and filename.endswith('.py'):
module_name = filename[:-3] # Remove .py extension moduleName = filename[:-3] # Remove .py extension
try: try:
# Import the module # Import the module
module = importlib.import_module(f'modules.connectors.{module_name}') module = importlib.import_module(f'modules.connectors.{moduleName}')
# Find connector classes (classes that inherit from BaseConnectorAi) # Find connector classes (classes that inherit from BaseConnectorAi)
for attr_name in dir(module): for attrName in dir(module):
attr = getattr(module, attr_name) attr = getattr(module, attrName)
if (isinstance(attr, type) and if (isinstance(attr, type) and
issubclass(attr, BaseConnectorAi) and issubclass(attr, BaseConnectorAi) and
attr != BaseConnectorAi): attr != BaseConnectorAi):
@ -71,12 +71,12 @@ class ModelRegistry:
"""Refresh models from all registered connectors.""" """Refresh models from all registered connectors."""
import time import time
current_time = time.time() currentTime = time.time()
# Check if refresh is needed # Check if refresh is needed
if (not force and if (not force and
self._last_refresh is not None and self._lastRefresh is not None and
current_time - self._last_refresh < self._refresh_interval): currentTime - self._lastRefresh < self._refreshInterval):
return return
logger.info("Refreshing model registry...") logger.info("Refreshing model registry...")
@ -94,7 +94,7 @@ class ModelRegistry:
except Exception as e: except Exception as e:
logger.error(f"Failed to refresh models from {connector.getConnectorType()}: {e}") logger.error(f"Failed to refresh models from {connector.getConnectorType()}: {e}")
self._last_refresh = current_time self._lastRefresh = currentTime
logger.info(f"Model registry refreshed: {len(self._models)} models available") logger.info(f"Model registry refreshed: {len(self._models)} models available")
def getModel(self, name: str) -> Optional[AiModel]: def getModel(self, name: str) -> Optional[AiModel]:
@ -107,29 +107,29 @@ class ModelRegistry:
self.refreshModels() self.refreshModels()
return list(self._models.values()) return list(self._models.values())
def getModelsByConnector(self, connector_type: str) -> List[AiModel]: def getModelsByConnector(self, connectorType: str) -> List[AiModel]:
"""Get models from a specific connector.""" """Get models from a specific connector."""
self.refreshModels() self.refreshModels()
return [model for model in self._models.values() if model.connectorType == connector_type] return [model for model in self._models.values() if model.connectorType == connectorType]
def getModelsByCapability(self, capability: str) -> List[AiModel]: def getModelsByCapability(self, capability: str) -> List[AiModel]:
"""Get models that support a specific capability.""" """Get models that support a specific capability."""
self.refreshModels() self.refreshModels()
return [model for model in self._models.values() if capability in model.capabilities] return [model for model in self._models.values() if capability in model.capabilities]
def getModelsByTag(self, tag: str) -> List[AiModel]: def getModelsByPriority(self, priority: str) -> List[AiModel]:
"""Get models that have a specific tag.""" """Get models that have a specific priority."""
self.refreshModels() self.refreshModels()
return [model for model in self._models.values() if tag in model.tags] return [model for model in self._models.values() if model.priority == priority]
def getAvailableModels(self) -> List[AiModel]: def getAvailableModels(self) -> List[AiModel]:
"""Get only available models.""" """Get only available models."""
self.refreshModels() self.refreshModels()
return [model for model in self._models.values() if model.isAvailable] return [model for model in self._models.values() if model.isAvailable]
def getConnectorForModel(self, model_name: str) -> Optional[BaseConnectorAi]: def getConnectorForModel(self, modelName: str) -> Optional[BaseConnectorAi]:
"""Get the connector instance for a specific model.""" """Get the connector instance for a specific model."""
model = self.getModel(model_name) model = self.getModel(modelName)
if model: if model:
return self._connectors.get(model.connectorType) return self._connectors.get(model.connectorType)
return None return None
@ -139,34 +139,34 @@ class ModelRegistry:
self.refreshModels() self.refreshModels()
stats = { stats = {
"total_models": len(self._models), "totalModels": len(self._models),
"available_models": len([m for m in self._models.values() if m.isAvailable]), "availableModels": len([m for m in self._models.values() if m.isAvailable]),
"connectors": len(self._connectors), "connectors": len(self._connectors),
"by_connector": {}, "byConnector": {},
"by_capability": {}, "byCapability": {},
"by_tag": {} "byPriority": {}
} }
# Count by connector # Count by connector
for model in self._models.values(): for model in self._models.values():
connector = model.connectorType connector = model.connectorType
if connector not in stats["by_connector"]: if connector not in stats["byConnector"]:
stats["by_connector"][connector] = 0 stats["byConnector"][connector] = 0
stats["by_connector"][connector] += 1 stats["byConnector"][connector] += 1
# Count by capability # Count by capability
for model in self._models.values(): for model in self._models.values():
for capability in model.capabilities: for capability in model.capabilities:
if capability not in stats["by_capability"]: if capability not in stats["byCapability"]:
stats["by_capability"][capability] = 0 stats["byCapability"][capability] = 0
stats["by_capability"][capability] += 1 stats["byCapability"][capability] += 1
# Count by tag # Count by priority
for model in self._models.values(): for model in self._models.values():
for tag in model.tags: priority = model.priority
if tag not in stats["by_tag"]: if priority not in stats["byPriority"]:
stats["by_tag"][tag] = 0 stats["byPriority"][priority] = 0
stats["by_tag"][tag] += 1 stats["byPriority"][priority] += 1
return stats return stats

View file

@ -3,24 +3,8 @@ Configuration for dynamic model selection rules.
This makes model selection configurable rather than hardcoded. This makes model selection configurable rather than hardcoded.
""" """
from typing import Dict, List, Any, Optional from typing import Dict, List, Any
from dataclasses import dataclass from modules.datamodels.datamodelAi import OperationTypeEnum, ModelCapabilitiesEnum, PriorityEnum, SelectionRule
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: class ModelSelectionConfig:
@ -28,148 +12,142 @@ class ModelSelectionConfig:
def __init__(self): def __init__(self):
self.rules = self._loadDefaultRules() self.rules = self._loadDefaultRules()
self.fallback_models = self._loadFallbackModels() self.fallbackModels = self._loadFallbackModels()
def _loadDefaultRules(self) -> List[SelectionRule]: def _loadDefaultRules(self) -> List[SelectionRule]:
"""Load default selection rules.""" """Load default selection rules."""
return [ return [
# High quality for planning and analysis # High quality for planning and analysis
SelectionRule( SelectionRule(
name="high_quality_analysis", name="highQualityAnalysis",
condition="Planning or analysis operations requiring high quality", condition="Planning or analysis operations requiring high quality",
weight=10.0, weight=10.0,
operation_types=[OperationType.GENERATE_PLAN, OperationType.ANALYSE_CONTENT], operationTypes=[OperationTypeEnum.PLAN, OperationTypeEnum.ANALYSE],
required_tags=[ModelTags.TEXT, ModelTags.REASONING, ModelTags.ANALYSIS], priority=PriorityEnum.QUALITY,
preferred_tags=[ModelTags.HIGH_QUALITY], capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.ANALYSIS],
avoid_tags=[ModelTags.FAST], minQualityRating=8
min_quality_rating=8
), ),
# Fast processing for basic operations # Fast processing for basic operations
SelectionRule( SelectionRule(
name="fast_basic_processing", name="fastBasicProcessing",
condition="Basic operations requiring speed", condition="Basic operations requiring speed",
weight=8.0, weight=8.0,
operation_types=[OperationType.GENERAL], operationTypes=[OperationTypeEnum.GENERAL],
required_tags=[ModelTags.TEXT, ModelTags.CHAT], priority=PriorityEnum.SPEED,
preferred_tags=[ModelTags.FAST], capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT],
avoid_tags=[], minQualityRating=5
min_quality_rating=5
), ),
# Cost-effective for high-volume operations # Cost-effective for high-volume operations
SelectionRule( SelectionRule(
name="cost_effective_processing", name="costEffectiveProcessing",
condition="High-volume operations where cost matters", condition="High-volume operations where cost matters",
weight=7.0, weight=7.0,
operation_types=[OperationType.GENERAL, OperationType.GENERATE_CONTENT], operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.GENERATE],
required_tags=[ModelTags.TEXT], priority=PriorityEnum.COST,
preferred_tags=[ModelTags.COST_EFFECTIVE], capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION],
avoid_tags=[], maxCost=0.01 # $0.01 per 1k tokens
max_cost=0.01 # $0.01 per 1k tokens
), ),
# Image analysis specific # Image analysis specific
SelectionRule( SelectionRule(
name="image_analysis", name="imageAnalyse",
condition="Image analysis operations", condition="Image analysis operations",
weight=10.0, weight=10.0,
operation_types=[OperationType.IMAGE_ANALYSIS], operationTypes=[OperationTypeEnum.IMAGE_ANALYSE],
required_tags=[ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL], priority=PriorityEnum.QUALITY,
preferred_tags=[ModelTags.HIGH_QUALITY], capabilities=[ModelCapabilitiesEnum.VISION, ModelCapabilitiesEnum.MULTIMODAL],
avoid_tags=[], minQualityRating=8
min_quality_rating=8
), ),
# Web research specific # Web research specific
SelectionRule( SelectionRule(
name="web_research", name="webResearch",
condition="Web research operations", condition="Web research operations",
weight=9.0, weight=9.0,
operation_types=[OperationType.WEB_RESEARCH], operationTypes=[OperationTypeEnum.WEB_RESEARCH],
required_tags=[ModelTags.TEXT, ModelTags.ANALYSIS], priority=PriorityEnum.BALANCED,
preferred_tags=[ModelTags.WEB, ModelTags.SEARCH], capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.ANALYSIS, ModelCapabilitiesEnum.WEB_SEARCH],
avoid_tags=[], minQualityRating=7
min_quality_rating=7
), ),
# Large context requirements # Large context requirements
SelectionRule( SelectionRule(
name="large_context", name="largeContext",
condition="Operations requiring large context", condition="Operations requiring large context",
weight=8.0, weight=8.0,
operation_types=[OperationType.GENERAL, OperationType.ANALYSE_CONTENT], operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.ANALYSE],
required_tags=[ModelTags.TEXT], priority=PriorityEnum.BALANCED,
preferred_tags=[], capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION],
avoid_tags=[], minContextLength=100000 # 100k tokens
min_context_length=100000 # 100k tokens
) )
] ]
def _loadFallbackModels(self) -> Dict[str, Dict[str, Any]]: def _loadFallbackModels(self) -> Dict[str, Dict[str, Any]]:
"""Load fallback model selection criteria.""" """Load fallback model selection criteria."""
return { return {
OperationType.GENERAL: { OperationTypeEnum.GENERAL: {
"priority_order": ["speed", "quality", "cost"], "priorityOrder": ["speed", "quality", "cost"],
"required_tags": [ModelTags.TEXT, ModelTags.CHAT], "operationTypes": [ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT],
"min_quality_rating": 5, "minQualityRating": 5,
"max_cost_per_1k": 0.01 "maxCostPer1k": 0.01
}, },
OperationType.IMAGE_ANALYSIS: { OperationTypeEnum.IMAGE_ANALYSE: {
"priority_order": ["quality", "speed"], "priorityOrder": ["quality", "speed"],
"required_tags": [ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL], "operationTypes": [ModelCapabilitiesEnum.VISION, ModelCapabilitiesEnum.MULTIMODAL],
"min_quality_rating": 8, "minQualityRating": 8,
"max_cost_per_1k": 0.1 "maxCostPer1k": 0.1
}, },
OperationType.IMAGE_GENERATION: { OperationTypeEnum.IMAGE_GENERATE: {
"priority_order": ["quality", "speed"], "priorityOrder": ["quality", "speed"],
"required_tags": [ModelTags.IMAGE_GENERATION, ModelTags.ART, ModelTags.VISUAL], "operationTypes": [ModelCapabilitiesEnum.IMAGE_GENERATE, ModelCapabilitiesEnum.ART, ModelCapabilitiesEnum.VISUAL_CREATION],
"min_quality_rating": 8, "minQualityRating": 8,
"max_cost_per_1k": 0.1 "maxCostPer1k": 0.1
}, },
OperationType.WEB_RESEARCH: { OperationTypeEnum.WEB_RESEARCH: {
"priority_order": ["quality", "speed", "cost"], "priorityOrder": ["quality", "speed", "cost"],
"required_tags": [ModelTags.TEXT, ModelTags.ANALYSIS], "operationTypes": [ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.ANALYSIS],
"preferred_tags": [ModelTags.WEB, ModelTags.SEARCH], "preferredTags": [ModelCapabilitiesEnum.WEB_SEARCH],
"min_quality_rating": 7, "minQualityRating": 7,
"max_cost_per_1k": 0.02 "maxCostPer1k": 0.02
}, },
OperationType.GENERATE_PLAN: { OperationTypeEnum.PLAN: {
"priority_order": ["quality", "speed"], "priorityOrder": ["quality", "speed"],
"required_tags": [ModelTags.TEXT, ModelTags.REASONING, ModelTags.ANALYSIS], "operationTypes": [ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.ANALYSIS],
"preferred_tags": [ModelTags.HIGH_QUALITY], "preferredTags": [PriorityEnum.QUALITY],
"min_quality_rating": 8, "minQualityRating": 8,
"max_cost_per_1k": 0.1 "maxCostPer1k": 0.1
}, },
OperationType.ANALYSE_CONTENT: { OperationTypeEnum.ANALYSE: {
"priority_order": ["quality", "speed"], "priorityOrder": ["quality", "speed"],
"required_tags": [ModelTags.TEXT, ModelTags.ANALYSIS, ModelTags.REASONING], "operationTypes": [ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.ANALYSIS, ModelCapabilitiesEnum.REASONING],
"preferred_tags": [ModelTags.HIGH_QUALITY], "preferredTags": [PriorityEnum.QUALITY],
"min_quality_rating": 8, "minQualityRating": 8,
"max_cost_per_1k": 0.1 "maxCostPer1k": 0.1
} }
} }
def getRulesForOperation(self, operation_type: str) -> List[SelectionRule]: def getRulesForOperation(self, operationType: str) -> List[SelectionRule]:
"""Get rules that apply to a specific operation type.""" """Get rules that apply to a specific operation type."""
return [rule for rule in self.rules if operation_type in rule.operation_types] return [rule for rule in self.rules if operationType in rule.operationTypes]
def getFallbackCriteria(self, operation_type: str) -> Dict[str, Any]: def getFallbackCriteria(self, operationType: str) -> Dict[str, Any]:
"""Get fallback selection criteria for a specific operation type.""" """Get fallback selection criteria for a specific operation type."""
return self.fallback_models.get(operation_type, self.fallback_models[OperationType.GENERAL]) return self.fallbackModels.get(operationType, self.fallbackModels[OperationTypeEnum.GENERAL])
def addRule(self, rule: SelectionRule): def addRule(self, rule: SelectionRule):
"""Add a new selection rule.""" """Add a new selection rule."""
self.rules.append(rule) self.rules.append(rule)
def removeRule(self, rule_name: str): def removeRule(self, ruleName: str):
"""Remove a selection rule by name.""" """Remove a selection rule by name."""
self.rules = [rule for rule in self.rules if rule.name != rule_name] self.rules = [rule for rule in self.rules if rule.name != ruleName]
def updateRule(self, rule_name: str, **kwargs): def updateRule(self, ruleName: str, **kwargs):
"""Update an existing rule.""" """Update an existing rule."""
for rule in self.rules: for rule in self.rules:
if rule.name == rule_name: if rule.name == ruleName:
for key, value in kwargs.items(): for key, value in kwargs.items():
if hasattr(rule, key): if hasattr(rule, key):
setattr(rule, key, value) setattr(rule, key, value)

View file

@ -4,7 +4,7 @@ Dynamic model selector using configurable rules and scoring.
import logging import logging
from typing import List, Optional, Dict, Any, Tuple from typing import List, Optional, Dict, Any, Tuple
from modules.datamodels.datamodelAi import AiModel, AiCallOptions, OperationType, Priority, ProcessingMode, ModelTags from modules.datamodels.datamodelAi import AiModel, AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum, ModelCapabilitiesEnum
from modules.aicore.aicoreModelSelectionConfig import model_selection_config from modules.aicore.aicoreModelSelectionConfig import model_selection_config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -20,7 +20,7 @@ class ModelSelector:
prompt: str, prompt: str,
context: str, context: str,
options: AiCallOptions, options: AiCallOptions,
available_models: List[AiModel]) -> Optional[AiModel]: availableModels: List[AiModel]) -> Optional[AiModel]:
""" """
Select the best model based on configurable rules and scoring. Select the best model based on configurable rules and scoring.
@ -28,171 +28,171 @@ class ModelSelector:
prompt: User prompt prompt: User prompt
context: Context data context: Context data
options: AI call options options: AI call options
available_models: List of available models to choose from availableModels: List of available models to choose from
Returns: Returns:
Selected model or None if no suitable model found Selected model or None if no suitable model found
""" """
if not available_models: if not availableModels:
logger.warning("No models available for selection") logger.warning("No models available for selection")
return None return None
logger.info(f"Selecting model for operation: {options.operationType}, priority: {options.priority}") logger.info(f"Selecting model for operation: {options.operationType}, priority: {options.priority}")
# Calculate input size # Calculate input size
input_size = len(prompt.encode("utf-8")) + len(context.encode("utf-8")) inputSize = len(prompt.encode("utf-8")) + len(context.encode("utf-8"))
# Get applicable rules # Get applicable rules
rules = self.config.getRulesForOperation(options.operationType) rules = self.config.getRulesForOperation(options.operationType)
logger.debug(f"Found {len(rules)} applicable rules for {options.operationType}") logger.debug(f"Found {len(rules)} applicable rules for {options.operationType}")
# Score each model # Score each model
scored_models = [] scoredModels = []
for model in available_models: for model in availableModels:
if not model.isAvailable: if not model.isAvailable:
continue continue
score = self._calculateModelScore(model, input_size, options, rules) score = self._calculateModelScore(model, inputSize, options, rules)
if score > 0: # Only consider models with positive scores if score > 0: # Only consider models with positive scores
scored_models.append((model, score)) scoredModels.append((model, score))
logger.debug(f"Model {model.name}: score={score:.2f}") logger.debug(f"Model {model.name}: score={score:.2f}")
if not scored_models: if not scoredModels:
logger.warning("No models passed the selection criteria, trying fallback criteria") logger.warning("No models passed the selection criteria, trying fallback criteria")
# Try fallback criteria # Try fallback criteria
fallback_criteria = self.getFallbackCriteria(options.operationType) fallbackCriteria = self.getFallbackCriteria(options.operationType)
return self._selectWithFallbackCriteria(available_models, fallback_criteria, input_size, options) return self._selectWithFallbackCriteria(availableModels, fallbackCriteria, inputSize, options)
# Sort by score (highest first) # Sort by score (highest first)
scored_models.sort(key=lambda x: x[1], reverse=True) scoredModels.sort(key=lambda x: x[1], reverse=True)
selected_model = scored_models[0][0] selectedModel = scoredModels[0][0]
selected_score = scored_models[0][1] selectedScore = scoredModels[0][1]
logger.info(f"Selected model: {selected_model.name} (score: {selected_score:.2f})") logger.info(f"Selected model: {selectedModel.name} (score: {selectedScore:.2f})")
# Log selection details # Log selection details
self._logSelectionDetails(selected_model, input_size, options) self._logSelectionDetails(selectedModel, inputSize, options)
return selected_model return selectedModel
def _calculateModelScore(self, def _calculateModelScore(self,
model: AiModel, model: AiModel,
input_size: int, inputSize: int,
options: AiCallOptions, options: AiCallOptions,
rules: List) -> float: rules: List) -> float:
"""Calculate score for a model based on rules and criteria.""" """Calculate score for a model based on rules and criteria."""
score = 0.0 score = 0.0
# Check basic requirements # Check basic requirements
if not self._meetsBasicRequirements(model, input_size, options): if not self._meetsBasicRequirements(model, inputSize, options):
return 0.0 return 0.0
# Apply rules # Apply rules
for rule in rules: for rule in rules:
rule_score = self._applyRule(model, input_size, options, rule) ruleScore = self._applyRule(model, inputSize, options, rule)
score += rule_score * rule.weight score += ruleScore * rule.weight
# Apply priority-based scoring # Apply priority-based scoring
priority_score = self._applyPriorityScoring(model, options) priorityScore = self._applyPriorityScoring(model, options)
score += priority_score score += priorityScore
# Apply processing mode scoring # Apply processing mode scoring
mode_score = self._applyProcessingModeScoring(model, options) modeScore = self._applyProcessingModeScoring(model, options)
score += mode_score score += modeScore
# Apply cost constraints # Apply cost constraints
if not self._meetsCostConstraints(model, input_size, options): if not self._meetsCostConstraints(model, inputSize, options):
score *= 0.1 # Heavily penalize but don't eliminate score *= 0.1 # Heavily penalize but don't eliminate
return max(0.0, score) return max(0.0, score)
def _meetsBasicRequirements(self, model: AiModel, input_size: int, options: AiCallOptions) -> bool: def _meetsBasicRequirements(self, model: AiModel, inputSize: int, options: AiCallOptions) -> bool:
"""Check if model meets basic requirements.""" """Check if model meets basic requirements."""
# Context length check # Context length check
if model.contextLength > 0 and input_size > model.contextLength * 0.8: if model.contextLength > 0 and inputSize > model.contextLength * 0.8:
logger.debug(f"Model {model.name} rejected: input too large ({input_size} > {model.contextLength * 0.8})") logger.debug(f"Model {model.name} rejected: input too large ({inputSize} > {model.contextLength * 0.8})")
return False return False
# Required tags check # Required operation types check
if options.requiredTags: if options.operationTypes:
if not all(tag in model.tags for tag in options.requiredTags): if not all(opType in model.operationTypes for opType in options.operationTypes):
logger.debug(f"Model {model.name} rejected: missing required tags") logger.debug(f"Model {model.name} rejected: missing required operation types")
return False return False
# Capabilities check # Capabilities check
if options.modelCapabilities: if options.capabilities:
if not all(cap in model.capabilities for cap in options.modelCapabilities): if not all(cap in model.capabilities for cap in options.capabilities):
logger.debug(f"Model {model.name} rejected: missing required capabilities") logger.debug(f"Model {model.name} rejected: missing required capabilities")
return False return False
# Avoid tags check # Avoid operation types check
for rule in self.config.getRulesForOperation(options.operationType): for rule in self.config.getRulesForOperation(options.operationType):
if any(tag in model.tags for tag in rule.avoid_tags): if any(opType in model.operationTypes for opType in rule.avoidOperationTypes):
logger.debug(f"Model {model.name} rejected: has avoid tags") logger.debug(f"Model {model.name} rejected: has avoid operation types")
return False return False
return True return True
def _applyRule(self, model: AiModel, input_size: int, options: AiCallOptions, rule) -> float: def _applyRule(self, model: AiModel, inputSize: int, options: AiCallOptions, rule) -> float:
"""Apply a specific rule to calculate score contribution.""" """Apply a specific rule to calculate score contribution."""
score = 0.0 score = 0.0
# Required tags match # Required operation types match
if all(tag in model.tags for tag in rule.required_tags): if all(opType in model.operationTypes for opType in rule.operationTypes):
score += 1.0 score += 1.0
# Preferred tags match # Preferred capabilities match
preferred_matches = sum(1 for tag in rule.preferred_tags if tag in model.tags) preferredMatches = sum(1 for cap in rule.preferredCapabilities if cap in model.capabilities)
if rule.preferred_tags: if rule.preferredCapabilities:
score += (preferred_matches / len(rule.preferred_tags)) * 0.5 score += (preferredMatches / len(rule.preferredCapabilities)) * 0.5
# Quality rating check # Quality rating check
if rule.min_quality_rating and model.qualityRating >= rule.min_quality_rating: if rule.minQualityRating and model.qualityRating >= rule.minQualityRating:
score += 0.3 score += 0.3
# Context length check # Context length check
if rule.min_context_length and model.contextLength >= rule.min_context_length: if rule.minContextLength and model.contextLength >= rule.minContextLength:
score += 0.2 score += 0.2
return score return score
def _applyPriorityScoring(self, model: AiModel, options: AiCallOptions) -> float: def _applyPriorityScoring(self, model: AiModel, options: AiCallOptions) -> float:
"""Apply priority-based scoring.""" """Apply priority-based scoring."""
if options.priority == Priority.SPEED: if options.priority == PriorityEnum.SPEED:
return model.speedRating * 0.1 return model.speedRating * 0.1
elif options.priority == Priority.QUALITY: elif options.priority == PriorityEnum.QUALITY:
return model.qualityRating * 0.1 return model.qualityRating * 0.1
elif options.priority == Priority.COST: elif options.priority == PriorityEnum.COST:
# Lower cost = higher score # Lower cost = higher score
cost_score = max(0, 1.0 - (model.costPer1kTokens * 1000)) costScore = max(0, 1.0 - (model.costPer1kTokensInput * 1000))
return cost_score * 0.1 return costScore * 0.1
else: # BALANCED else: # BALANCED
return (model.qualityRating + model.speedRating) * 0.05 return (model.qualityRating + model.speedRating) * 0.05
def _applyProcessingModeScoring(self, model: AiModel, options: AiCallOptions) -> float: def _applyProcessingModeScoring(self, model: AiModel, options: AiCallOptions) -> float:
"""Apply processing mode scoring.""" """Apply processing mode scoring."""
if options.processingMode == ProcessingMode.DETAILED: if options.processingMode == ProcessingModeEnum.DETAILED:
if ModelTags.HIGH_QUALITY in model.tags: if model.priority == PriorityEnum.QUALITY:
return 0.2 return 0.2
elif options.processingMode == ProcessingMode.BASIC: elif options.processingMode == ProcessingModeEnum.BASIC:
if ModelTags.FAST in model.tags: if model.priority == PriorityEnum.SPEED:
return 0.2 return 0.2
return 0.0 return 0.0
def _meetsCostConstraints(self, model: AiModel, input_size: int, options: AiCallOptions) -> bool: def _meetsCostConstraints(self, model: AiModel, inputSize: int, options: AiCallOptions) -> bool:
"""Check if model meets cost constraints.""" """Check if model meets cost constraints."""
if options.maxCost is None: if options.maxCost is None:
return True return True
# Estimate cost # Estimate cost
estimated_tokens = input_size / 4 estimatedTokens = inputSize / 4
estimated_cost = (estimated_tokens / 1000) * model.costPer1kTokens estimatedCost = (estimatedTokens / 1000) * model.costPer1kTokensInput
return estimated_cost <= options.maxCost return estimatedCost <= options.maxCost
def _logSelectionDetails(self, model: AiModel, input_size: int, options: AiCallOptions): def _logSelectionDetails(self, model: AiModel, inputSize: int, options: AiCallOptions):
"""Log detailed selection information.""" """Log detailed selection information."""
logger.info(f"Model Selection Details:") logger.info(f"Model Selection Details:")
logger.info(f" Selected: {model.displayName} ({model.name})") logger.info(f" Selected: {model.displayName} ({model.name})")
@ -200,50 +200,50 @@ class ModelSelector:
logger.info(f" Operation: {options.operationType}") logger.info(f" Operation: {options.operationType}")
logger.info(f" Priority: {options.priority}") logger.info(f" Priority: {options.priority}")
logger.info(f" Processing Mode: {options.processingMode}") logger.info(f" Processing Mode: {options.processingMode}")
logger.info(f" Input Size: {input_size} bytes") logger.info(f" Input Size: {inputSize} bytes")
logger.info(f" Context Length: {model.contextLength}") logger.info(f" Context Length: {model.contextLength}")
logger.info(f" Max Tokens: {model.maxTokens}") logger.info(f" Max Tokens: {model.maxTokens}")
logger.info(f" Quality Rating: {model.qualityRating}/10") logger.info(f" Quality Rating: {model.qualityRating}/10")
logger.info(f" Speed Rating: {model.speedRating}/10") logger.info(f" Speed Rating: {model.speedRating}/10")
logger.info(f" Cost: ${model.costPer1kTokens:.4f}/1k tokens") logger.info(f" Cost: ${model.costPer1kTokensInput:.4f}/1k tokens")
logger.info(f" Capabilities: {', '.join(model.capabilities)}") logger.info(f" Capabilities: {', '.join(model.capabilities)}")
logger.info(f" Tags: {', '.join(model.tags)}") logger.info(f" Priority: {model.priority}")
def getFallbackCriteria(self, operation_type: str) -> Dict[str, Any]: def getFallbackCriteria(self, operationType: str) -> Dict[str, Any]:
"""Get fallback selection criteria for an operation type.""" """Get fallback selection criteria for an operation type."""
return self.config.getFallbackCriteria(operation_type) return self.config.getFallbackCriteria(operationType)
def _selectWithFallbackCriteria(self, def _selectWithFallbackCriteria(self,
available_models: List[AiModel], availableModels: List[AiModel],
fallback_criteria: Dict[str, Any], fallbackCriteria: Dict[str, Any],
input_size: int, inputSize: int,
options: AiCallOptions) -> Optional[AiModel]: options: AiCallOptions) -> Optional[AiModel]:
"""Select model using fallback criteria when normal selection fails.""" """Select model using fallback criteria when normal selection fails."""
logger.info("Using fallback criteria for model selection") logger.info("Using fallback criteria for model selection")
# Filter models by fallback criteria # Filter models by fallback criteria
candidates = [] candidates = []
for model in available_models: for model in availableModels:
if not model.isAvailable: if not model.isAvailable:
continue continue
# Check required tags # Check required operation types
if fallback_criteria.get("required_tags"): if fallbackCriteria.get("operationTypes"):
if not all(tag in model.tags for tag in fallback_criteria["required_tags"]): if not all(opType in model.operationTypes for opType in fallbackCriteria["operationTypes"]):
continue continue
# Check quality rating # Check quality rating
if fallback_criteria.get("min_quality_rating"): if fallbackCriteria.get("minQualityRating"):
if model.qualityRating < fallback_criteria["min_quality_rating"]: if model.qualityRating < fallbackCriteria["minQualityRating"]:
continue continue
# Check cost # Check cost
if fallback_criteria.get("max_cost_per_1k"): if fallbackCriteria.get("maxCostPer1k"):
if model.costPer1kTokens > fallback_criteria["max_cost_per_1k"]: if model.costPer1kTokensInput > fallbackCriteria["maxCostPer1k"]:
continue continue
# Check context length # Check context length
if model.contextLength > 0 and input_size > model.contextLength * 0.8: if model.contextLength > 0 and inputSize > model.contextLength * 0.8:
continue continue
candidates.append(model) candidates.append(model)
@ -253,26 +253,133 @@ class ModelSelector:
return None return None
# Sort by priority order from fallback criteria # Sort by priority order from fallback criteria
priority_order = fallback_criteria.get("priority_order", ["quality", "speed", "cost"]) priorityOrder = fallbackCriteria.get("priorityOrder", ["quality", "speed", "cost"])
def get_priority_score(model: AiModel) -> float: def _getPriorityScore(model: AiModel) -> float:
score = 0.0 score = 0.0
for i, priority in enumerate(priority_order): for i, priority in enumerate(priorityOrder):
weight = len(priority_order) - i # Higher weight for earlier priorities weight = len(priorityOrder) - i # Higher weight for earlier priorities
if priority == "quality": if priority == "quality":
score += model.qualityRating * weight score += model.qualityRating * weight
elif priority == "speed": elif priority == "speed":
score += model.speedRating * weight score += model.speedRating * weight
elif priority == "cost": elif priority == "cost":
# Lower cost = higher score # Lower cost = higher score
score += (1.0 - model.costPer1kTokens * 1000) * weight score += (1.0 - model.costPer1kTokensInput * 1000) * weight
return score return score
candidates.sort(key=get_priority_score, reverse=True) candidates.sort(key=_getPriorityScore, reverse=True)
selected_model = candidates[0] selectedModel = candidates[0]
logger.info(f"Fallback selection: {selected_model.name} (score: {get_priority_score(selected_model):.2f})") logger.info(f"Fallback selection: {selectedModel.name} (score: {_getPriorityScore(selectedModel):.2f})")
return selected_model return selectedModel
def getFallbackModels(self,
prompt: str,
context: str,
options: AiCallOptions,
availableModels: List[AiModel]) -> List[AiModel]:
"""
Get prioritized list of models for fallback sequence.
Steps:
1. Filter models by capability requirements
2. Rate models by business requirements (priority, processing mode)
3. Sort by rating (descending), then by cost (ascending)
Args:
prompt: User prompt
context: Context data
options: AI call options
availableModels: List of available models
Returns:
Prioritized list of models for fallback sequence
"""
if not availableModels:
logger.warning("No models available for fallback selection")
return []
logger.info(f"Building fallback sequence for operation: {options.operationType}, priority: {options.priority}")
# Step 1: Filter by capability requirements
capableModels = self._filterByCapabilities(availableModels, options)
logger.info(f"Step 1 - Capable models: {[m.name for m in capableModels]}")
if not capableModels:
logger.warning("No models meet capability requirements")
return []
# Step 2: Rate models by business requirements
ratedModels = self._rateModelsByBusinessRequirements(capableModels, prompt, context, options)
logger.info(f"Step 2 - Rated models: {[(m.name, rating) for m, rating in ratedModels]}")
# Step 3: Sort by rating (descending), then by cost (ascending)
sortedModels = self._sortModelsByRatingAndCost(ratedModels)
logger.info(f"Step 3 - Sorted fallback sequence: {[m.name for m in sortedModels]}")
return sortedModels
def _filterByCapabilities(self, models: List[AiModel], options: AiCallOptions) -> List[AiModel]:
"""Filter models by required capabilities."""
capableModels = []
for model in models:
if not model.isAvailable:
continue
# Check if model supports required capabilities
if options.capabilities:
if not all(cap in model.capabilities for cap in options.capabilities):
logger.debug(f"Model {model.name} missing required capabilities: {options.capabilities}")
continue
# Check operation type compatibility
if not self._meetsBasicRequirements(model, options):
logger.debug(f"Model {model.name} doesn't meet basic requirements")
continue
capableModels.append(model)
return capableModels
def _rateModelsByBusinessRequirements(self,
models: List[AiModel],
prompt: str,
context: str,
options: AiCallOptions) -> List[Tuple[AiModel, float]]:
"""Rate models based on business requirements (priority, processing mode)."""
ratedModels = []
inputSize = len(prompt.encode("utf-8")) + len(context.encode("utf-8"))
for model in models:
# Base score from model selection logic
baseScore = self._calculateModelScore(model, inputSize, options, [])
# Apply priority-based scoring
priorityScore = self._applyPriorityScoring(model, options)
# Apply processing mode scoring
processingScore = self._applyProcessingModeScoring(model, options)
# Combine scores
totalScore = baseScore + priorityScore + processingScore
ratedModels.append((model, totalScore))
logger.debug(f"Model {model.name}: base={baseScore:.2f}, priority={priorityScore:.2f}, processing={processingScore:.2f}, total={totalScore:.2f}")
return ratedModels
def _sortModelsByRatingAndCost(self, ratedModels: List[Tuple[AiModel, float]]) -> List[AiModel]:
"""Sort models by rating (descending), then by cost (ascending)."""
def sortKey(item):
model, rating = item
# Primary sort: rating (descending)
# Secondary sort: cost (ascending)
return (-rating, model.costPer1kTokensInput)
sortedItems = sorted(ratedModels, key=sortKey)
return [model for model, rating in sortedItems]
# Global selector instance # Global selector instance

View file

@ -5,7 +5,7 @@ from typing import Dict, Any, List, Union
from fastapi import HTTPException from fastapi import HTTPException
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.aicore.aicoreBase import BaseConnectorAi from modules.aicore.aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelAi import AiModel, ModelTags from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -55,17 +55,17 @@ class AiAnthropic(BaseConnectorAi):
connectorType="anthropic", connectorType="anthropic",
maxTokens=200000, maxTokens=200000,
contextLength=200000, contextLength=200000,
costPer1kTokens=0.015, costPer1kTokensInput=0.015,
costPer1kTokensOutput=0.075, costPer1kTokensOutput=0.075,
speedRating=7, speedRating=7,
qualityRating=10, qualityRating=10,
capabilities=["text_generation", "chat", "reasoning", "analysis"], capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.ANALYSIS],
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.ANALYSIS, ModelTags.HIGH_QUALITY],
functionCall=self.callAiBasic, functionCall=self.callAiBasic,
priority="quality", priority=PriorityEnum.QUALITY,
processingMode="detailed", processingMode=ProcessingModeEnum.DETAILED,
preferredFor=["generate_plan", "analyse_content"], operationTypes=[OperationTypeEnum.PLAN, OperationTypeEnum.ANALYSE],
version="claude-3-5-sonnet-20241022" version="claude-3-5-sonnet-20241022",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
), ),
AiModel( AiModel(
name="anthropic_callAiImage", name="anthropic_callAiImage",
@ -73,17 +73,17 @@ class AiAnthropic(BaseConnectorAi):
connectorType="anthropic", connectorType="anthropic",
maxTokens=200000, maxTokens=200000,
contextLength=200000, contextLength=200000,
costPer1kTokens=0.015, costPer1kTokensInput=0.015,
costPer1kTokensOutput=0.075, costPer1kTokensOutput=0.075,
speedRating=7, speedRating=7,
qualityRating=10, qualityRating=10,
capabilities=["image_analysis", "vision", "multimodal"], capabilities=[ModelCapabilitiesEnum.IMAGE_ANALYSE, ModelCapabilitiesEnum.VISION, ModelCapabilitiesEnum.MULTIMODAL],
tags=[ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL, ModelTags.HIGH_QUALITY],
functionCall=self.callAiImage, functionCall=self.callAiImage,
priority="quality", priority=PriorityEnum.QUALITY,
processingMode="detailed", processingMode=ProcessingModeEnum.DETAILED,
preferredFor=["image_analysis"], operationTypes=[OperationTypeEnum.IMAGE_ANALYSE],
version="claude-3-5-sonnet-20241022" version="claude-3-5-sonnet-20241022",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
) )
] ]

View file

@ -0,0 +1,233 @@
import logging
from typing import Dict, Any, List, Union
from modules.aicore.aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum
# Configure logger
logger = logging.getLogger(__name__)
class AiInternal(BaseConnectorAi):
"""Internal connector for document processing, generation, and rendering."""
def __init__(self):
super().__init__()
logger.info("Internal Connector initialized")
def getConnectorType(self) -> str:
"""Get the connector type identifier."""
return "internal"
def getModels(self) -> List[AiModel]:
"""Get all available internal models."""
return [
AiModel(
name="internal_extraction",
displayName="Internal Document Extractor",
connectorType="internal",
maxTokens=0, # Not token-based
contextLength=0,
costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0,
speedRating=8,
qualityRating=8,
capabilities=[ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.TEXT_EXTRACTION],
functionCall=self.extractDocument,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
operationTypes=[OperationTypeEnum.GENERAL],
version="internal-extractor-v1",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.001 + (bytesSent + bytesReceived) / (1024 * 1024) * 0.01
),
AiModel(
name="internal_generation",
displayName="Internal Document Generator",
connectorType="internal",
maxTokens=0, # Not token-based
contextLength=0,
costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0,
speedRating=7,
qualityRating=8,
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.ANALYSIS],
functionCall=self.generateDocument,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
operationTypes=[OperationTypeEnum.GENERATE],
version="internal-generator-v1",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.002 + (bytesReceived / (1024 * 1024)) * 0.005
),
AiModel(
name="internal_rendering",
displayName="Internal Document Renderer",
connectorType="internal",
maxTokens=0, # Not token-based
contextLength=0,
costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0,
speedRating=6,
qualityRating=9,
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.ANALYSIS],
functionCall=self.renderDocument,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=[OperationTypeEnum.GENERATE],
version="internal-renderer-v1",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.003 + (bytesReceived / (1024 * 1024)) * 0.008
)
]
async def extractDocument(self, documentData: Union[str, bytes], extractionType: str = "basic") -> Dict[str, Any]:
"""
Extract content from a document.
Args:
documentData: The document data to extract from
extractionType: Type of extraction (basic, advanced, detailed)
Returns:
Dictionary with extraction results
"""
try:
logger.info(f"Starting document extraction with type: {extractionType}")
# Simulate document extraction processing
# In a real implementation, this would use actual document processing libraries
if isinstance(documentData, bytes):
content = documentData.decode('utf-8', errors='ignore')
else:
content = str(documentData)
# Basic extraction logic
extractedContent = {
"text": content,
"metadata": {
"extraction_type": extractionType,
"content_length": len(content),
"processing_time": 0.1 # Simulated
}
}
logger.info(f"Document extraction completed successfully")
return extractedContent
except Exception as e:
logger.error(f"Error during document extraction: {str(e)}")
return {
"error": str(e),
"success": False
}
async def generateDocument(self, template: str, data: Dict[str, Any], format: str = "html") -> Dict[str, Any]:
"""
Generate a document from a template and data.
Args:
template: The document template
data: Data to populate the template
format: Output format (html, pdf, docx, etc.)
Returns:
Dictionary with generated document
"""
try:
logger.info(f"Starting document generation with format: {format}")
# Simulate document generation processing
# In a real implementation, this would use actual templating engines
# Basic template processing
generatedContent = template
for key, value in data.items():
placeholder = f"{{{key}}}"
generatedContent = generatedContent.replace(placeholder, str(value))
result = {
"content": generatedContent,
"format": format,
"metadata": {
"template_length": len(template),
"data_keys": list(data.keys()),
"processing_time": 0.2 # Simulated
}
}
logger.info(f"Document generation completed successfully")
return result
except Exception as e:
logger.error(f"Error during document generation: {str(e)}")
return {
"error": str(e),
"success": False
}
async def renderDocument(self, content: str, targetFormat: str, options: Dict[str, Any] = None) -> Dict[str, Any]:
"""
Render a document to a specific format.
Args:
content: The content to render
targetFormat: Target format (html, pdf, docx, etc.)
options: Rendering options
Returns:
Dictionary with rendered document
"""
try:
logger.info(f"Starting document rendering to format: {targetFormat}")
if options is None:
options = {}
# Simulate document rendering processing
# In a real implementation, this would use actual rendering libraries
# Basic rendering logic based on target format
if targetFormat.lower() == "html":
renderedContent = f"<html><body>{content}</body></html>"
elif targetFormat.lower() == "pdf":
# Simulate PDF rendering
renderedContent = f"PDF_CONTENT_PLACEHOLDER: {content}"
else:
# Default to plain text
renderedContent = content
result = {
"content": renderedContent,
"format": targetFormat,
"metadata": {
"input_length": len(content),
"output_length": len(renderedContent),
"processing_time": 0.3, # Simulated
"options": options
}
}
logger.info(f"Document rendering completed successfully")
return result
except Exception as e:
logger.error(f"Error during document rendering: {str(e)}")
return {
"error": str(e),
"success": False
}
async def _testConnection(self) -> bool:
"""
Tests the internal processing capabilities.
Returns:
True if internal processing is working, False otherwise
"""
try:
# Test basic functionality
testContent = "Test document content"
result = await self.extractDocument(testContent)
return result.get("success", True) and "error" not in result
except Exception as e:
logger.error(f"Internal connector test failed: {str(e)}")
return False

View file

@ -5,7 +5,7 @@ from typing import Dict, Any, List, Union
from fastapi import HTTPException from fastapi import HTTPException
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.aicore.aicoreBase import BaseConnectorAi from modules.aicore.aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelAi import AiModel, ModelTags from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -57,17 +57,17 @@ class AiOpenai(BaseConnectorAi):
connectorType="openai", connectorType="openai",
maxTokens=128000, maxTokens=128000,
contextLength=128000, contextLength=128000,
costPer1kTokens=0.03, costPer1kTokensInput=0.03,
costPer1kTokensOutput=0.06, costPer1kTokensOutput=0.06,
speedRating=8, speedRating=8,
qualityRating=9, qualityRating=9,
capabilities=["text_generation", "chat", "reasoning", "analysis"], capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.ANALYSIS],
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.ANALYSIS],
functionCall=self.callAiBasic, functionCall=self.callAiBasic,
priority="balanced", priority=PriorityEnum.BALANCED,
processingMode="advanced", processingMode=ProcessingModeEnum.ADVANCED,
preferredFor=["general", "analyse_content"], operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.ANALYSE],
version="gpt-4o" version="gpt-4o",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
), ),
AiModel( AiModel(
name="openai_callAiBasic_gpt35", name="openai_callAiBasic_gpt35",
@ -75,17 +75,17 @@ class AiOpenai(BaseConnectorAi):
connectorType="openai", connectorType="openai",
maxTokens=16000, maxTokens=16000,
contextLength=16000, contextLength=16000,
costPer1kTokens=0.0015, costPer1kTokensInput=0.0015,
costPer1kTokensOutput=0.002, costPer1kTokensOutput=0.002,
speedRating=9, speedRating=9,
qualityRating=7, qualityRating=7,
capabilities=["text_generation", "chat", "reasoning"], capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING],
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.FAST, ModelTags.COST_EFFECTIVE],
functionCall=self.callAiBasic, functionCall=self.callAiBasic,
priority="speed", priority=PriorityEnum.SPEED,
processingMode="basic", processingMode=ProcessingModeEnum.BASIC,
preferredFor=["general"], operationTypes=[OperationTypeEnum.GENERAL],
version="gpt-3.5-turbo" version="gpt-3.5-turbo",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0015 + (bytesReceived / 4 / 1000) * 0.002
), ),
AiModel( AiModel(
name="openai_callAiImage", name="openai_callAiImage",
@ -93,17 +93,17 @@ class AiOpenai(BaseConnectorAi):
connectorType="openai", connectorType="openai",
maxTokens=128000, maxTokens=128000,
contextLength=128000, contextLength=128000,
costPer1kTokens=0.03, costPer1kTokensInput=0.03,
costPer1kTokensOutput=0.06, costPer1kTokensOutput=0.06,
speedRating=7, speedRating=7,
qualityRating=9, qualityRating=9,
capabilities=["image_analysis", "vision", "multimodal"], capabilities=[ModelCapabilitiesEnum.IMAGE_ANALYSE, ModelCapabilitiesEnum.VISION, ModelCapabilitiesEnum.MULTIMODAL],
tags=[ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL],
functionCall=self.callAiImage, functionCall=self.callAiImage,
priority="quality", priority=PriorityEnum.QUALITY,
processingMode="detailed", processingMode=ProcessingModeEnum.DETAILED,
preferredFor=["image_analysis"], operationTypes=[OperationTypeEnum.IMAGE_ANALYSE],
version="gpt-4o" version="gpt-4o",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
), ),
AiModel( AiModel(
name="openai_generateImage", name="openai_generateImage",
@ -111,17 +111,17 @@ class AiOpenai(BaseConnectorAi):
connectorType="openai", connectorType="openai",
maxTokens=0, # Image generation doesn't use tokens maxTokens=0, # Image generation doesn't use tokens
contextLength=0, contextLength=0,
costPer1kTokens=0.04, costPer1kTokensInput=0.04,
costPer1kTokensOutput=0.0, costPer1kTokensOutput=0.0,
speedRating=6, speedRating=6,
qualityRating=9, qualityRating=9,
capabilities=["image_generation", "art", "visual_creation"], capabilities=[ModelCapabilitiesEnum.IMAGE_GENERATE, ModelCapabilitiesEnum.ART, ModelCapabilitiesEnum.VISUAL_CREATION],
tags=[ModelTags.IMAGE_GENERATION, ModelTags.ART, ModelTags.VISUAL],
functionCall=self.generateImage, functionCall=self.generateImage,
priority="quality", priority=PriorityEnum.QUALITY,
processingMode="detailed", processingMode=ProcessingModeEnum.DETAILED,
preferredFor=["image_generation"], operationTypes=[OperationTypeEnum.IMAGE_GENERATE],
version="dall-e-3" version="dall-e-3",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04
) )
] ]

View file

@ -5,7 +5,7 @@ from typing import Dict, Any, List, Union, Optional
from fastapi import HTTPException from fastapi import HTTPException
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.aicore.aicoreBase import BaseConnectorAi from modules.aicore.aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelAi import AiModel, ModelTags from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -55,17 +55,17 @@ class AiPerplexity(BaseConnectorAi):
connectorType="perplexity", connectorType="perplexity",
maxTokens=128000, maxTokens=128000,
contextLength=128000, contextLength=128000,
costPer1kTokens=0.005, costPer1kTokensInput=0.005,
costPer1kTokensOutput=0.005, costPer1kTokensOutput=0.005,
speedRating=8, speedRating=8,
qualityRating=8, qualityRating=8,
capabilities=["text_generation", "chat", "reasoning", "web_search"], capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.WEB_SEARCH],
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.WEB, ModelTags.SEARCH, ModelTags.COST_EFFECTIVE],
functionCall=self.callAiBasic, functionCall=self.callAiBasic,
priority="balanced", priority=PriorityEnum.BALANCED,
processingMode="advanced", processingMode=ProcessingModeEnum.ADVANCED,
preferredFor=["general", "web_research"], operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.WEB_RESEARCH],
version="llama-3.1-sonar-large-128k-online" version="llama-3.1-sonar-large-128k-online",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005
), ),
AiModel( AiModel(
name="perplexity_callAiWithWebSearch", name="perplexity_callAiWithWebSearch",
@ -73,17 +73,17 @@ class AiPerplexity(BaseConnectorAi):
connectorType="perplexity", connectorType="perplexity",
maxTokens=128000, maxTokens=128000,
contextLength=128000, contextLength=128000,
costPer1kTokens=0.01, costPer1kTokensInput=0.01,
costPer1kTokensOutput=0.01, costPer1kTokensOutput=0.01,
speedRating=7, speedRating=7,
qualityRating=9, qualityRating=9,
capabilities=["text_generation", "web_search", "research"], capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.RESEARCH],
tags=[ModelTags.TEXT, ModelTags.WEB, ModelTags.SEARCH, ModelTags.RESEARCH, ModelTags.HIGH_QUALITY],
functionCall=self.callAiWithWebSearch, functionCall=self.callAiWithWebSearch,
priority="quality", priority=PriorityEnum.QUALITY,
processingMode="detailed", processingMode=ProcessingModeEnum.DETAILED,
preferredFor=["web_research"], operationTypes=[OperationTypeEnum.WEB_RESEARCH],
version="sonar-pro" version="sonar-pro",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01
), ),
AiModel( AiModel(
name="perplexity_researchTopic", name="perplexity_researchTopic",
@ -91,17 +91,17 @@ class AiPerplexity(BaseConnectorAi):
connectorType="perplexity", connectorType="perplexity",
maxTokens=32000, maxTokens=32000,
contextLength=32000, contextLength=32000,
costPer1kTokens=0.002, costPer1kTokensInput=0.002,
costPer1kTokensOutput=0.002, costPer1kTokensOutput=0.002,
speedRating=8, speedRating=8,
qualityRating=8, qualityRating=8,
capabilities=["web_search", "research", "information_gathering"], capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.RESEARCH, ModelCapabilitiesEnum.INFORMATION_GATHERING],
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.RESEARCH, ModelTags.INFORMATION, ModelTags.COST_EFFECTIVE],
functionCall=self.researchTopic, functionCall=self.researchTopic,
priority="cost", priority=PriorityEnum.COST,
processingMode="basic", processingMode=ProcessingModeEnum.BASIC,
preferredFor=["web_research"], operationTypes=[OperationTypeEnum.WEB_RESEARCH],
version="mistral-7b-instruct" version="mistral-7b-instruct",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
), ),
AiModel( AiModel(
name="perplexity_answerQuestion", name="perplexity_answerQuestion",
@ -109,17 +109,17 @@ class AiPerplexity(BaseConnectorAi):
connectorType="perplexity", connectorType="perplexity",
maxTokens=32000, maxTokens=32000,
contextLength=32000, contextLength=32000,
costPer1kTokens=0.002, costPer1kTokensInput=0.002,
costPer1kTokensOutput=0.002, costPer1kTokensOutput=0.002,
speedRating=8, speedRating=8,
qualityRating=8, qualityRating=8,
capabilities=["web_search", "question_answering", "research"], capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.QUESTION_ANSWERING, ModelCapabilitiesEnum.RESEARCH],
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.RESEARCH, ModelTags.COST_EFFECTIVE],
functionCall=self.answerQuestion, functionCall=self.answerQuestion,
priority="cost", priority=PriorityEnum.COST,
processingMode="basic", processingMode=ProcessingModeEnum.BASIC,
preferredFor=["web_research"], operationTypes=[OperationTypeEnum.WEB_RESEARCH],
version="mistral-7b-instruct" version="mistral-7b-instruct",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
), ),
AiModel( AiModel(
name="perplexity_getCurrentNews", name="perplexity_getCurrentNews",
@ -127,17 +127,17 @@ class AiPerplexity(BaseConnectorAi):
connectorType="perplexity", connectorType="perplexity",
maxTokens=32000, maxTokens=32000,
contextLength=32000, contextLength=32000,
costPer1kTokens=0.002, costPer1kTokensInput=0.002,
costPer1kTokensOutput=0.002, costPer1kTokensOutput=0.002,
speedRating=8, speedRating=8,
qualityRating=8, qualityRating=8,
capabilities=["web_search", "news", "current_events"], capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.NEWS, ModelCapabilitiesEnum.CURRENT_EVENTS],
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.COST_EFFECTIVE],
functionCall=self.getCurrentNews, functionCall=self.getCurrentNews,
priority="cost", priority=PriorityEnum.COST,
processingMode="basic", processingMode=ProcessingModeEnum.BASIC,
preferredFor=["web_research"], operationTypes=[OperationTypeEnum.WEB_RESEARCH],
version="mistral-7b-instruct" version="mistral-7b-instruct",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
) )
] ]

View file

@ -9,7 +9,7 @@ from tavily import AsyncTavilyClient
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp
from modules.aicore.aicoreBase import BaseConnectorAi from modules.aicore.aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelAi import AiModel, ModelTags from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum
from modules.datamodels.datamodelWeb import ( from modules.datamodels.datamodelWeb import (
WebSearchActionResult, WebSearchActionResult,
WebSearchActionDocument, WebSearchActionDocument,
@ -46,9 +46,9 @@ class ConnectorWeb(BaseConnectorAi):
super().__init__() super().__init__()
self.client: Optional[AsyncTavilyClient] = None self.client: Optional[AsyncTavilyClient] = None
# Cached settings loaded at initialization time # Cached settings loaded at initialization time
self.crawl_timeout: int = 30 self.crawlTimeout: int = 30
self.crawl_max_retries: int = 3 self.crawlMaxRetries: int = 3
self.crawl_retry_delay: int = 2 self.crawlRetryDelay: int = 2
# Cached web search constraints (camelCase per project style) # Cached web search constraints (camelCase per project style)
self.webSearchMinResults: int = 1 self.webSearchMinResults: int = 1
self.webSearchMaxResults: int = 20 self.webSearchMaxResults: int = 20
@ -66,17 +66,17 @@ class ConnectorWeb(BaseConnectorAi):
connectorType="tavily", connectorType="tavily",
maxTokens=0, # Web search doesn't use tokens maxTokens=0, # Web search doesn't use tokens
contextLength=0, contextLength=0,
costPer1kTokens=0.0, costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0, costPer1kTokensOutput=0.0,
speedRating=8, speedRating=8,
qualityRating=8, qualityRating=8,
capabilities=["web_search", "information_retrieval", "url_discovery"], capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.INFORMATION_RETRIEVAL, ModelCapabilitiesEnum.URL_DISCOVERY],
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.INFORMATION],
functionCall=self.search, functionCall=self.search,
priority="balanced", priority=PriorityEnum.BALANCED,
processingMode="basic", processingMode=ProcessingModeEnum.BASIC,
preferredFor=["web_research"], operationTypes=[OperationTypeEnum.WEB_RESEARCH],
version="tavily-search" version="tavily-search",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived, searchDepth="basic", numRequests=1: numRequests * (1 if searchDepth == "basic" else 2) * 0.008
), ),
AiModel( AiModel(
name="tavily_extract", name="tavily_extract",
@ -84,17 +84,17 @@ class ConnectorWeb(BaseConnectorAi):
connectorType="tavily", connectorType="tavily",
maxTokens=0, # Web extraction doesn't use tokens maxTokens=0, # Web extraction doesn't use tokens
contextLength=0, contextLength=0,
costPer1kTokens=0.0, costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0, costPer1kTokensOutput=0.0,
speedRating=6, speedRating=6,
qualityRating=8, qualityRating=8,
capabilities=["web_crawling", "content_extraction", "text_extraction"], capabilities=[ModelCapabilitiesEnum.WEB_CRAWLING, ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.TEXT_EXTRACTION],
tags=[ModelTags.WEB, ModelTags.EXTRACT, ModelTags.CONTENT],
functionCall=self.crawl, functionCall=self.crawl,
priority="balanced", priority=PriorityEnum.BALANCED,
processingMode="basic", processingMode=ProcessingModeEnum.BASIC,
preferredFor=["web_research"], operationTypes=[OperationTypeEnum.WEB_RESEARCH],
version="tavily-extract" version="tavily-extract",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived, extractionDepth="basic", numSuccessfulUrls=1: (numSuccessfulUrls / 5) * (1 if extractionDepth == "basic" else 2) * 0.008
), ),
AiModel( AiModel(
name="tavily_crawl", name="tavily_crawl",
@ -102,17 +102,17 @@ class ConnectorWeb(BaseConnectorAi):
connectorType="tavily", connectorType="tavily",
maxTokens=0, # Web crawling doesn't use tokens maxTokens=0, # Web crawling doesn't use tokens
contextLength=0, contextLength=0,
costPer1kTokens=0.0, costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0, costPer1kTokensOutput=0.0,
speedRating=6, speedRating=6,
qualityRating=8, qualityRating=8,
capabilities=["web_crawling", "content_extraction", "mapping"], capabilities=[ModelCapabilitiesEnum.WEB_CRAWLING, ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.MAPPING],
tags=[ModelTags.WEB, ModelTags.CRAWL, ModelTags.EXTRACT],
functionCall=self.crawl, functionCall=self.crawl,
priority="balanced", priority=PriorityEnum.BALANCED,
processingMode="basic", processingMode=ProcessingModeEnum.BASIC,
preferredFor=["web_research"], operationTypes=[OperationTypeEnum.WEB_RESEARCH],
version="tavily-crawl" version="tavily-crawl",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived, numPages=10, extractionDepth="basic", withInstructions=False, numSuccessfulExtractions=10: ((numPages / 10) * (2 if withInstructions else 1) + (numSuccessfulExtractions / 5) * (1 if extractionDepth == "basic" else 2)) * 0.008
), ),
AiModel( AiModel(
name="tavily_scrape", name="tavily_scrape",
@ -120,17 +120,17 @@ class ConnectorWeb(BaseConnectorAi):
connectorType="tavily", connectorType="tavily",
maxTokens=0, # Web scraping doesn't use tokens maxTokens=0, # Web scraping doesn't use tokens
contextLength=0, contextLength=0,
costPer1kTokens=0.0, costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0, costPer1kTokensOutput=0.0,
speedRating=6, speedRating=6,
qualityRating=8, qualityRating=8,
capabilities=["web_search", "web_crawling", "content_extraction", "information_retrieval"], capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.WEB_CRAWLING, ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.INFORMATION_RETRIEVAL],
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.CRAWL, ModelTags.EXTRACT, ModelTags.CONTENT, ModelTags.INFORMATION],
functionCall=self.scrape, functionCall=self.scrape,
priority="balanced", priority=PriorityEnum.BALANCED,
processingMode="basic", processingMode=ProcessingModeEnum.BASIC,
preferredFor=["web_research"], operationTypes=[OperationTypeEnum.WEB_RESEARCH],
version="tavily-search-extract" version="tavily-search-extract",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived, searchDepth="basic", numSuccessfulUrls=1, extractionDepth="basic": ((1 if searchDepth == "basic" else 2) + (numSuccessfulUrls / 5) * (1 if extractionDepth == "basic" else 2)) * 0.008
) )
] ]
@ -140,14 +140,14 @@ class ConnectorWeb(BaseConnectorAi):
if not api_key: if not api_key:
raise ValueError("Tavily API key not configured. Please set Connector_WebTavily_API_KEY_SECRET in config.ini") raise ValueError("Tavily API key not configured. Please set Connector_WebTavily_API_KEY_SECRET in config.ini")
# Load and cache web crawl related configuration # Load and cache web crawl related configuration
crawl_timeout = int(APP_CONFIG.get("Web_Crawl_TIMEOUT", "30")) crawlTimeout = int(APP_CONFIG.get("Web_Crawl_TIMEOUT", "30"))
crawl_max_retries = int(APP_CONFIG.get("Web_Crawl_MAX_RETRIES", "3")) crawlMaxRetries = int(APP_CONFIG.get("Web_Crawl_MAX_RETRIES", "3"))
crawl_retry_delay = int(APP_CONFIG.get("Web_Crawl_RETRY_DELAY", "2")) crawlRetryDelay = int(APP_CONFIG.get("Web_Crawl_RETRY_DELAY", "2"))
return cls( return cls(
client=AsyncTavilyClient(api_key=api_key), client=AsyncTavilyClient(api_key=api_key),
crawl_timeout=crawl_timeout, crawlTimeout=crawlTimeout,
crawl_max_retries=crawl_max_retries, crawlMaxRetries=crawlMaxRetries,
crawl_retry_delay=crawl_retry_delay, crawlRetryDelay=crawlRetryDelay,
webSearchMinResults=int(APP_CONFIG.get("Web_Search_MIN_RESULTS", "1")), webSearchMinResults=int(APP_CONFIG.get("Web_Search_MIN_RESULTS", "1")),
webSearchMaxResults=int(APP_CONFIG.get("Web_Search_MAX_RESULTS", "20")), webSearchMaxResults=int(APP_CONFIG.get("Web_Search_MAX_RESULTS", "20")),
) )
@ -363,10 +363,10 @@ class ConnectorWeb(BaseConnectorAi):
) -> list[WebSearchResult]: ) -> list[WebSearchResult]:
"""Calls the Tavily API to perform a web search.""" """Calls the Tavily API to perform a web search."""
# Make sure max_results is within the allowed range (use cached values) # Make sure max_results is within the allowed range (use cached values)
min_results = self.webSearchMinResults minResults = self.webSearchMinResults
max_allowed_results = self.webSearchMaxResults maxAllowedResults = self.webSearchMaxResults
if max_results < min_results or max_results > max_allowed_results: if max_results < minResults or max_results > maxAllowedResults:
raise ValueError(f"max_results must be between {min_results} and {max_allowed_results}") raise ValueError(f"max_results must be between {minResults} and {maxAllowedResults}")
# Perform actual API call # Perform actual API call
# Build kwargs only for provided options to avoid API rejections # Build kwargs only for provided options to avoid API rejections
@ -409,16 +409,16 @@ class ConnectorWeb(BaseConnectorAi):
format: str | None = None, format: str | None = None,
) -> list[WebCrawlResult]: ) -> list[WebCrawlResult]:
"""Calls the Tavily API to extract text content from URLs with retry logic.""" """Calls the Tavily API to extract text content from URLs with retry logic."""
max_retries = self.crawl_max_retries maxRetries = self.crawlMaxRetries
retry_delay = self.crawl_retry_delay retryDelay = self.crawlRetryDelay
timeout = self.crawl_timeout timeout = self.crawlTimeout
logger.debug(f"Starting crawl of {len(urls)} URLs: {urls}") logger.debug(f"Starting crawl of {len(urls)} URLs: {urls}")
logger.debug(f"Crawl settings: extract_depth={extract_depth}, format={format}, timeout={timeout}s") logger.debug(f"Crawl settings: extract_depth={extract_depth}, format={format}, timeout={timeout}s")
for attempt in range(max_retries + 1): for attempt in range(maxRetries + 1):
try: try:
logger.debug(f"Crawl attempt {attempt + 1}/{max_retries + 1}") logger.debug(f"Crawl attempt {attempt + 1}/{maxRetries + 1}")
# Use asyncio.wait_for for timeout # Use asyncio.wait_for for timeout
# Build kwargs for extract # Build kwargs for extract
@ -460,11 +460,11 @@ class ConnectorWeb(BaseConnectorAi):
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.warning(f"Crawl attempt {attempt + 1} timed out after {timeout} seconds for URLs: {urls}") logger.warning(f"Crawl attempt {attempt + 1} timed out after {timeout} seconds for URLs: {urls}")
if attempt < max_retries: if attempt < maxRetries:
logger.info(f"Retrying in {retry_delay} seconds...") logger.info(f"Retrying in {retryDelay} seconds...")
await asyncio.sleep(retry_delay) await asyncio.sleep(retryDelay)
else: else:
raise Exception(f"Crawl failed after {max_retries + 1} attempts due to timeout") raise Exception(f"Crawl failed after {maxRetries + 1} attempts due to timeout")
except Exception as e: except Exception as e:
logger.warning(f"Crawl attempt {attempt + 1} failed for URLs {urls}: {str(e)}") logger.warning(f"Crawl attempt {attempt + 1} failed for URLs {urls}: {str(e)}")
@ -483,8 +483,8 @@ class ConnectorWeb(BaseConnectorAi):
if len(url) > 2000: if len(url) > 2000:
logger.debug(f" WARNING: URL is very long ({len(url)} chars)") logger.debug(f" WARNING: URL is very long ({len(url)} chars)")
if attempt < max_retries: if attempt < maxRetries:
logger.info(f"Retrying in {retry_delay} seconds...") logger.info(f"Retrying in {retryDelay} seconds...")
await asyncio.sleep(retry_delay) await asyncio.sleep(retryDelay)
else: else:
raise Exception(f"Crawl failed after {max_retries + 1} attempts: {str(e)}") raise Exception(f"Crawl failed after {maxRetries + 1} attempts: {str(e)}")

View file

@ -1,84 +1,63 @@
from typing import Optional, List, Dict, Any, Literal, Callable from typing import Optional, List, Dict, Any, Literal, Callable
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from enum import Enum
# Operation Types # Operation Types
class OperationType: class OperationTypeEnum(str, Enum):
GENERAL = "general" GENERAL = "general"
GENERATE_PLAN = "generate_plan" PLAN = "plan"
ANALYSE_CONTENT = "analyse_content" ANALYSE = "analyse"
GENERATE_CONTENT = "generate_content" GENERATE = "generate"
WEB_RESEARCH = "web_research" WEB_RESEARCH = "webResearch"
IMAGE_ANALYSIS = "image_analysis" IMAGE_ANALYSE = "imageAnalyse"
IMAGE_GENERATION = "image_generation" IMAGE_GENERATE = "imageGenerate"
# Processing Modes # Processing Modes
class ProcessingMode: class ProcessingModeEnum(str, Enum):
BASIC = "basic" BASIC = "basic"
ADVANCED = "advanced" ADVANCED = "advanced"
DETAILED = "detailed" DETAILED = "detailed"
# Priority Levels # Priority Levels
class Priority: class PriorityEnum(str, Enum):
SPEED = "speed" SPEED = "speed"
QUALITY = "quality" QUALITY = "quality"
COST = "cost" COST = "cost"
BALANCED = "balanced" BALANCED = "balanced"
# Model Tags # Model Capabilities Enumeration
class ModelTags: class ModelCapabilitiesEnum(str, Enum):
# Core capabilities # Text generation capabilities
TEXT = "text" TEXT_GENERATION = "text_generation"
CHAT = "chat" CHAT = "chat"
REASONING = "reasoning" REASONING = "reasoning"
ANALYSIS = "analysis" ANALYSIS = "analysis"
IMAGE = "image"
# Image capabilities
IMAGE_ANALYSE = "imageAnalyse"
IMAGE_GENERATE = "imageGenerate"
VISION = "vision" VISION = "vision"
MULTIMODAL = "multimodal" MULTIMODAL = "multimodal"
WEB = "web"
SEARCH = "search"
CRAWL = "crawl"
EXTRACT = "extract"
CONTENT = "content"
INFORMATION = "information"
# Quality indicators
HIGH_QUALITY = "high_quality"
FAST = "fast"
COST_EFFECTIVE = "cost_effective"
GENERAL = "general"
# Specialized capabilities
IMAGE_GENERATION = "image_generation"
ART = "art" ART = "art"
VISUAL = "visual" VISUAL_CREATION = "visual_creation"
VARIATIONS = "variations"
API = "api"
INFO = "info"
MODELS = "models"
# Web capabilities
WEB_SEARCH = "web_search"
WEB_CRAWLING = "web_crawling"
CONTENT_EXTRACTION = "content_extraction"
TEXT_EXTRACTION = "text_extraction"
INFORMATION_RETRIEVAL = "information_retrieval"
URL_DISCOVERY = "url_discovery"
MAPPING = "mapping"
# Operation Type to Required Tags Mapping # Research capabilities
OPERATION_TAG_MAPPING = { RESEARCH = "research"
OperationType.GENERAL: [ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING], QUESTION_ANSWERING = "question_answering"
OperationType.GENERATE_PLAN: [ModelTags.TEXT, ModelTags.REASONING, ModelTags.ANALYSIS], INFORMATION_GATHERING = "information_gathering"
OperationType.ANALYSE_CONTENT: [ModelTags.TEXT, ModelTags.ANALYSIS, ModelTags.REASONING], NEWS = "news"
OperationType.GENERATE_CONTENT: [ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING], CURRENT_EVENTS = "current_events"
OperationType.WEB_RESEARCH: [ModelTags.TEXT, ModelTags.ANALYSIS, ModelTags.REASONING],
OperationType.IMAGE_ANALYSIS: [ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL],
OperationType.IMAGE_GENERATION: [ModelTags.IMAGE_GENERATION, ModelTags.ART, ModelTags.VISUAL],
}
# Processing Mode to Priority Mapping
PROCESSING_MODE_PRIORITY_MAPPING = {
ProcessingMode.BASIC: Priority.SPEED,
ProcessingMode.ADVANCED: Priority.BALANCED,
ProcessingMode.DETAILED: Priority.QUALITY,
}
class AiModel(BaseModel): class AiModel(BaseModel):
@ -94,69 +73,61 @@ class AiModel(BaseModel):
contextLength: int = Field(description="Maximum context length this model can handle") contextLength: int = Field(description="Maximum context length this model can handle")
# Cost information # Cost information
costPer1kTokens: float = Field(default=0.0, description="Cost per 1000 input tokens") costPer1kTokensInput: float = Field(default=0.0, description="Cost per 1000 input tokens")
costPer1kTokensOutput: float = Field(default=0.0, description="Cost per 1000 output tokens") costPer1kTokensOutput: float = Field(default=0.0, description="Cost per 1000 output tokens")
# Performance ratings # Performance ratings
speedRating: int = Field(ge=1, le=10, description="Speed rating (1-10, higher = faster)") 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)") 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) # Function reference (not serialized)
functionCall: Optional[Callable] = Field(default=None, exclude=True, description="Function to call for this model") functionCall: Optional[Callable] = Field(default=None, exclude=True, description="Function to call for this model")
calculatePriceUsd: Optional[Callable] = Field(default=None, exclude=True, description="Function to calculate price in USD")
# Selection criteria # Selection criteria
priority: str = Field(default="balanced", description="Default priority for this model") capabilities: List[ModelCapabilitiesEnum] = Field(description="List of model capabilities. See ModelCapabilitiesEnum enum for available values.")
processingMode: str = Field(default="basic", description="Default processing mode") priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Default priority for this model. See PriorityEnum for available values.")
isAvailable: bool = Field(default=True, description="Whether model is currently available") processingMode: ProcessingModeEnum = Field(default=ProcessingModeEnum.BASIC, description="Default processing mode. See ProcessingModeEnum for available values.")
operationTypes: List[OperationTypeEnum] = Field(default=[], description="Operation types this model should avoid")
# Advanced selection criteria
minContextLength: Optional[int] = Field(default=None, description="Minimum context length required") 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") isAvailable: bool = Field(default=True, description="Whether model is currently available")
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 # Metadata
version: Optional[str] = Field(default=None, description="Model version") version: Optional[str] = Field(default=None, description="Model version")
lastUpdated: Optional[str] = Field(default=None, description="Last update timestamp") lastUpdated: Optional[str] = Field(default=None, description="Last update timestamp")
class Config: class Config:
arbitrary_types_allowed = True # Allow Callable type arbitraryTypesAllowed = True # Allow Callable type
class ModelCapabilities(BaseModel): class SelectionRule(BaseModel):
"""Model capabilities and characteristics for dynamic selection.""" """A rule for model selection."""
name: str = Field(description="Rule name identifier")
name: str = Field(description="Model name/identifier") condition: str = Field(description="Description of when this rule applies")
maxTokens: int = Field(description="Maximum token limit for this model") weight: float = Field(description="Weight for scoring (higher = more important)")
capabilities: List[str] = Field(description="List of capabilities: text, image, vision, reasoning, analysis, etc.") operationTypes: List[OperationTypeEnum] = Field(description="Operation types this rule applies to")
costPerToken: float = Field(default=0.0, description="Cost per token (if available)") priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Priority level for this rule")
processingTime: float = Field(default=1.0, description="Average processing time multiplier") capabilities: List[ModelCapabilitiesEnum] = Field(default=[], description="Required capabilities for this rule")
isAvailable: bool = Field(default=True, description="Whether model is currently available") minQualityRating: Optional[int] = Field(default=None, description="Minimum quality rating")
maxCost: Optional[float] = Field(default=None, description="Maximum cost threshold")
minContextLength: Optional[int] = Field(default=None, description="Minimum context length required")
class AiCallOptions(BaseModel): class AiCallOptions(BaseModel):
"""Options for centralized AI processing with clear operation types and tags.""" """Options for centralized AI processing with clear operation types and tags."""
operationType: OperationTypeEnum = Field(default=OperationTypeEnum.GENERAL, description="Type of operation")
operationType: str = Field(default="general", description="Type of operation: general, generate_plan, analyse_content, generate_content, web_research") priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Priority level")
priority: str = Field(default="balanced", description="speed|quality|cost|balanced")
compressPrompt: bool = Field(default=True, description="Whether to compress the prompt") compressPrompt: bool = Field(default=True, description="Whether to compress the prompt")
compressContext: bool = Field(default=True, description="If False: process each chunk; If True: summarize and work on summary") compressContext: bool = Field(default=True, description="If False: process each chunk; If True: summarize and work on summary")
processDocumentsIndividually: bool = Field(default=True, description="If True, process each document separately; else pool docs") processDocumentsIndividually: bool = Field(default=True, description="If True, process each document separately; else pool docs")
maxContextBytes: Optional[int] = Field(default=None, description="Hard cap for extracted context size passed to the model") maxContextBytes: Optional[int] = Field(default=None, description="Hard cap for extracted context size passed to the model")
maxCost: Optional[float] = Field(default=None, description="Max cost budget") maxCost: Optional[float] = Field(default=None, description="Max cost budget")
maxProcessingTime: Optional[int] = Field(default=None, description="Max processing time in seconds") maxProcessingTime: Optional[int] = Field(default=None, description="Max processing time in seconds")
requiredTags: Optional[List[str]] = Field(default=None, description="Required model tags for selection") processingMode: ProcessingModeEnum = Field(default=ProcessingModeEnum.BASIC, description="Processing mode")
processingMode: str = Field(default="basic", description="Processing mode: basic, advanced, detailed")
resultFormat: Optional[str] = Field(default=None, description="Expected result format: txt, json, csv, xml, etc.") resultFormat: Optional[str] = Field(default=None, description="Expected result format: txt, json, csv, xml, etc.")
# New fields for dynamic strategy
callType: Literal["planning", "text"] = Field(default="text", description="Call type: planning or text")
safetyMargin: float = Field(default=0.1, ge=0.0, le=0.5, description="Safety margin for token limits (0.0-0.5)") safetyMargin: float = Field(default=0.1, ge=0.0, le=0.5, description="Safety margin for token limits (0.0-0.5)")
modelCapabilities: Optional[List[str]] = Field(default=None, description="Required model capabilities for filtering") capabilities: Optional[List[ModelCapabilitiesEnum]] = Field(default=None, description="Required model capabilities for filtering")
# Model generation parameters # Model generation parameters
temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0, description="Temperature for response generation (0.0-2.0, lower = more consistent)") temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0, description="Temperature for response generation (0.0-2.0, lower = more consistent)")

File diff suppressed because it is too large Load diff

View file

@ -6,19 +6,14 @@ import time
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# No more hardcoded imports - everything is discovered dynamically
from modules.aicore.aicoreModelRegistry import model_registry from modules.aicore.aicoreModelRegistry import model_registry
from modules.aicore.aicoreModelSelector import model_selector from modules.aicore.aicoreModelSelector import model_selector
from modules.datamodels.datamodelAi import ( from modules.datamodels.datamodelAi import (
AiModel,
AiCallOptions, AiCallOptions,
AiCallRequest, AiCallRequest,
AiCallResponse, AiCallResponse,
OperationType, OperationTypeEnum,
ProcessingMode,
Priority,
ModelTags,
OPERATION_TAG_MAPPING,
PROCESSING_MODE_PRIORITY_MAPPING
) )
from modules.datamodels.datamodelWeb import ( from modules.datamodels.datamodelWeb import (
WebResearchRequest, WebResearchRequest,
@ -47,14 +42,14 @@ class AiObjects:
logger.info("Auto-discovering AI connectors...") logger.info("Auto-discovering AI connectors...")
# Use the model registry's built-in discovery mechanism # Use the model registry's built-in discovery mechanism
discovered_connectors = model_registry.discoverConnectors() discoveredConnectors = model_registry.discoverConnectors()
# Register each discovered connector # Register each discovered connector
for connector in discovered_connectors: for connector in discoveredConnectors:
model_registry.registerConnector(connector) model_registry.registerConnector(connector)
logger.info(f"Registered connector: {connector.getConnectorType()}") logger.info(f"Registered connector: {connector.getConnectorType()}")
logger.info(f"Total connectors registered: {len(discovered_connectors)}") logger.info(f"Total connectors registered: {len(discoveredConnectors)}")
logger.info("All AI connectors registered with dynamic model registry") logger.info("All AI connectors registered with dynamic model registry")
@classmethod @classmethod
@ -68,25 +63,25 @@ class AiObjects:
def _selectModel(self, prompt: str, context: str, options: AiCallOptions) -> str: def _selectModel(self, prompt: str, context: str, options: AiCallOptions) -> str:
"""Select the best model using dynamic model selection system.""" """Select the best model using dynamic model selection system."""
# Get available models from the dynamic registry # Get available models from the dynamic registry
available_models = model_registry.getAvailableModels() availableModels = model_registry.getAvailableModels()
if not available_models: if not availableModels:
logger.error("No models available in the registry") logger.error("No models available in the registry")
raise ValueError("No AI models available") raise ValueError("No AI models available")
# Use the dynamic model selector # Use the dynamic model selector
selected_model = model_selector.selectModel(prompt, context, options, available_models) selectedModel = model_selector.selectModel(prompt, context, options, availableModels)
if not selected_model: if not selectedModel:
logger.error("No suitable model found for the given criteria") logger.error("No suitable model found for the given criteria")
raise ValueError("No suitable AI model found") raise ValueError("No suitable AI model found")
logger.info(f"Selected model: {selected_model.name} ({selected_model.displayName})") logger.info(f"Selected model: {selectedModel.name} ({selectedModel.displayName})")
return selected_model.name return selectedModel.name
async def call(self, request: AiCallRequest) -> AiCallResponse: async def call(self, request: AiCallRequest) -> AiCallResponse:
"""Call AI model for text generation using dynamic model selection.""" """Call AI model for text generation with fallback mechanism."""
prompt = request.prompt prompt = request.prompt
context = request.context or "" context = request.context or ""
@ -96,45 +91,92 @@ class AiObjects:
inputBytes = len((prompt + context).encode("utf-8")) inputBytes = len((prompt + context).encode("utf-8"))
# Compress optionally (prompt/context) - simple truncation fallback kept here # Compress optionally (prompt/context) - simple truncation fallback kept here
def maybeTruncate(text: str, limit: int) -> str: def _maybeTruncate(text: str, limit: int) -> str:
data = text.encode("utf-8") data = text.encode("utf-8")
if len(data) <= limit: if len(data) <= limit:
return text return text
return data[:limit].decode("utf-8", errors="ignore") + "... [truncated]" return data[:limit].decode("utf-8", errors="ignore") + "... [truncated]"
if options.compressPrompt and len(prompt.encode("utf-8")) > 2000: if options.compressPrompt and len(prompt.encode("utf-8")) > 2000:
prompt = maybeTruncate(prompt, 2000) prompt = _maybeTruncate(prompt, 2000)
if options.compressContext and len(context.encode("utf-8")) > 70000: if options.compressContext and len(context.encode("utf-8")) > 70000:
context = maybeTruncate(context, 70000) context = _maybeTruncate(context, 70000)
# Derive generation parameters # Derive generation parameters
temperature = getattr(options, "temperature", None) temperature = getattr(options, "temperature", None)
if temperature is None: if temperature is None:
temperature = 0.2 temperature = 0.2
maxTokens = getattr(options, "maxTokens", None) maxTokens = getattr(options, "maxTokens", None)
# Don't set artificial limits - let the model use its full context length
# Our continuation system handles stopping early via prompt engineering
# Get fallback models for this operation type
availableModels = model_registry.getAvailableModels()
fallbackModels = model_selector.getFallbackModels(prompt, context, options, availableModels)
if not fallbackModels:
errorMsg = f"No suitable models found for operation {options.operationType}"
logger.error(errorMsg)
return AiCallResponse(
content=errorMsg,
modelName="error",
priceUsd=0.0,
processingTime=0.0,
bytesSent=inputBytes,
bytesReceived=0,
errorCount=1
)
# Try each model in fallback sequence
lastError = None
for attempt, model in enumerate(fallbackModels):
try: try:
# Select the best model using dynamic selection logger.info(f"Attempting AI call with model: {model.name} (attempt {attempt + 1}/{len(fallbackModels)})")
modelName = self._selectModel(prompt, context, options)
selectedModel = model_registry.getModel(modelName)
if not selectedModel: # Call the model
raise ValueError(f"Selected model {modelName} not found in registry") response = await self._callWithModel(model, prompt, context, temperature, maxTokens, inputBytes)
# Replace <TOKEN_LIMIT> placeholder in prompt for this specific model
context_length = selectedModel.contextLength logger.info(f"✅ AI call successful with model: {model.name}")
if context_length > 0: return response
token_limit = str(context_length)
except Exception as e:
lastError = e
logger.warning(f"❌ AI call failed with model {model.name}: {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...")
continue
else: else:
token_limit = "16000" # Default for text generation # All models failed
logger.error(f"💥 All {len(fallbackModels)} models failed for operation {options.operationType}")
break
# All fallback attempts failed - return error response
errorMsg = f"All AI models failed for operation {options.operationType}. Last error: {str(lastError)}"
logger.error(errorMsg)
return AiCallResponse(
content=errorMsg,
modelName="error",
priceUsd=0.0,
processingTime=0.0,
bytesSent=inputBytes,
bytesReceived=0,
errorCount=1
)
async def _callWithModel(self, model: AiModel, prompt: str, context: str, temperature: float, maxTokens: int, inputBytes: int) -> AiCallResponse:
"""Call a specific model and return the response."""
# Replace <TOKEN_LIMIT> placeholder in prompt for this specific model
contextLength = model.contextLength
if contextLength > 0:
tokenLimit = str(contextLength)
else:
tokenLimit = "16000" # Default for text generation
# Create a copy of the prompt for this model call # Create a copy of the prompt for this model call
modelPrompt = prompt modelPrompt = prompt
if "<TOKEN_LIMIT>" in modelPrompt: if "<TOKEN_LIMIT>" in modelPrompt:
modelPrompt = modelPrompt.replace("<TOKEN_LIMIT>", token_limit) modelPrompt = modelPrompt.replace("<TOKEN_LIMIT>", tokenLimit)
logger.debug(f"Replaced <TOKEN_LIMIT> with {token_limit} for model {modelName}") logger.debug(f"Replaced <TOKEN_LIMIT> with {tokenLimit} for model {model.name}")
# Update messages array with replaced content # Update messages array with replaced content
messages = [] messages = []
@ -146,33 +188,33 @@ class AiObjects:
startTime = time.time() startTime = time.time()
# Get the connector for this model # Get the connector for this model
connector = model_registry.getConnectorForModel(modelName) connector = model_registry.getConnectorForModel(model.name)
if not connector: if not connector:
raise ValueError(f"No connector found for model {modelName}") raise ValueError(f"No connector found for model {model.name}")
# Call the model's function directly # Call the model's function directly
if selectedModel.functionCall: if model.functionCall:
# Use the model's function call directly # Use the model's function call directly
if modelName.startswith("perplexity_callAiWithWebSearch"): if model.name.startswith("perplexity_callAiWithWebSearch"):
query = modelPrompt query = modelPrompt
if context: if context:
query = f"Context: {context}\n\nQuery: {modelPrompt}" query = f"Context: {context}\n\nQuery: {modelPrompt}"
content = await selectedModel.functionCall(query, temperature=temperature, maxTokens=maxTokens) content = await model.functionCall(query, temperature=temperature, maxTokens=maxTokens)
elif modelName.startswith("perplexity_researchTopic"): elif model.name.startswith("perplexity_researchTopic"):
content = await selectedModel.functionCall(modelPrompt) content = await model.functionCall(modelPrompt)
elif modelName.startswith("perplexity_answerQuestion"): elif model.name.startswith("perplexity_answerQuestion"):
content = await selectedModel.functionCall(modelPrompt, context) content = await model.functionCall(modelPrompt, context)
elif modelName.startswith("perplexity_getCurrentNews"): elif model.name.startswith("perplexity_getCurrentNews"):
content = await selectedModel.functionCall(modelPrompt) content = await model.functionCall(modelPrompt)
else: else:
# Standard callAiBasic # Standard callAiBasic
if selectedModel.connectorType == "anthropic": if model.connectorType == "anthropic":
response = await selectedModel.functionCall(messages, temperature=temperature, maxTokens=maxTokens) response = await model.functionCall(messages, temperature=temperature, maxTokens=maxTokens)
content = response["choices"][0]["message"]["content"] content = response["choices"][0]["message"]["content"]
else: else:
content = await selectedModel.functionCall(messages, temperature=temperature, maxTokens=maxTokens) content = await model.functionCall(messages, temperature=temperature, maxTokens=maxTokens)
else: else:
raise ValueError(f"Model {modelName} has no function call defined") raise ValueError(f"Model {model.name} has no function call defined")
# Calculate timing and output bytes # Calculate timing and output bytes
endTime = time.time() endTime = time.time()
@ -180,68 +222,92 @@ class AiObjects:
outputBytes = len(content.encode("utf-8")) outputBytes = len(content.encode("utf-8"))
# Calculate price using model's cost information # Calculate price using model's cost information
estimated_tokens = inputBytes / 4 priceUsd = model.costPer1kTokensInput * (inputBytes / 4 / 1000) + model.costPer1kTokensOutput * (outputBytes / 4 / 1000)
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( return AiCallResponse(
success=True,
content=content, content=content,
model=modelName, modelName=model.name,
processingTime=processingTime,
priceUsd=priceUsd, priceUsd=priceUsd,
processingTime=processingTime,
bytesSent=inputBytes, bytesSent=inputBytes,
bytesReceived=outputBytes, bytesReceived=outputBytes,
errorCount=0 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: async def callImage(self, prompt: str, imageData: Union[str, bytes], mimeType: str = None, options: AiCallOptions = None) -> AiCallResponse:
"""Call AI model for image analysis with fallback mechanism.""" """Call AI model for image analysis with fallback mechanism."""
if options is None: if options is None:
options = AiCallOptions(operationType=OperationType.IMAGE_ANALYSIS) options = AiCallOptions(operationType=OperationTypeEnum.IMAGE_ANALYSE)
# Calculate input bytes (prompt + image data) # Calculate input bytes (prompt + image data)
inputBytes = len(prompt.encode("utf-8")) + len(imageData) if isinstance(imageData, bytes) else len(prompt.encode("utf-8")) + len(str(imageData).encode("utf-8")) inputBytes = len(prompt.encode("utf-8")) + len(imageData) if isinstance(imageData, bytes) else len(prompt.encode("utf-8")) + len(str(imageData).encode("utf-8"))
# Get fallback models for image analysis
availableModels = model_registry.getAvailableModels()
fallbackModels = model_selector.getFallbackModels(prompt, "", options, availableModels)
if not fallbackModels:
errorMsg = f"No suitable models found for image analysis"
logger.error(errorMsg)
return AiCallResponse(
content=errorMsg,
modelName="error",
priceUsd=0.0,
processingTime=0.0,
bytesSent=inputBytes,
bytesReceived=0,
errorCount=1
)
# Try each model in fallback sequence
lastError = None
for attempt, model in enumerate(fallbackModels):
try: try:
# Select the best model for image analysis logger.info(f"Attempting image analysis with model: {model.name} (attempt {attempt + 1}/{len(fallbackModels)})")
modelName = self._selectModel(prompt, "", options)
selectedModel = model_registry.getModel(modelName)
if not selectedModel: # Call the model
raise ValueError(f"Selected model {modelName} not found in registry") response = await self._callImageWithModel(model, prompt, imageData, mimeType, inputBytes)
# Get the connector for this model logger.info(f"✅ Image analysis successful with model: {model.name}")
connector = model_registry.getConnectorForModel(modelName) return response
if not connector:
raise ValueError(f"No connector found for model {modelName}")
except Exception as e:
lastError = e
logger.warning(f"❌ Image analysis failed with model {model.name}: {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
)
async def _callImageWithModel(self, model: AiModel, prompt: str, imageData: Union[str, bytes], mimeType: str, inputBytes: int) -> AiCallResponse:
"""Call a specific model for image analysis and return the response."""
# Start timing # Start timing
startTime = time.time() startTime = time.time()
# Call the model's function directly # Call the model's function directly
if selectedModel.functionCall: if model.functionCall:
content = await selectedModel.functionCall(prompt, imageData, mimeType) content = await model.functionCall(prompt, imageData, mimeType)
else: else:
raise ValueError(f"Model {modelName} has no function call defined") raise ValueError(f"Model {model.name} has no function call defined")
# Calculate timing and output bytes # Calculate timing and output bytes
endTime = time.time() endTime = time.time()
@ -249,39 +315,23 @@ class AiObjects:
outputBytes = len(content.encode("utf-8")) outputBytes = len(content.encode("utf-8"))
# Calculate price using model's cost information # Calculate price using model's cost information
estimated_tokens = inputBytes / 4 priceUsd = model.costPer1kTokensInput * (inputBytes / 4 / 1000) + model.costPer1kTokensOutput * (outputBytes / 4 / 1000)
priceUsd = (estimated_tokens / 1000) * selectedModel.costPer1kTokens + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
logger.info(f"✅ Image analysis successful with model: {modelName}")
return AiCallResponse( return AiCallResponse(
success=True,
content=content, content=content,
model=modelName, modelName=model.name,
processingTime=processingTime,
priceUsd=priceUsd, priceUsd=priceUsd,
processingTime=processingTime,
bytesSent=inputBytes, bytesSent=inputBytes,
bytesReceived=outputBytes, bytesReceived=outputBytes,
errorCount=0 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: async def generateImage(self, prompt: str, size: str = "1024x1024", quality: str = "standard", style: str = "vivid", options: AiCallOptions = None) -> AiCallResponse:
"""Generate an image using AI.""" """Generate an image using AI."""
if options is None: if options is None:
options = AiCallOptions(operationType=OperationType.IMAGE_GENERATION) options = AiCallOptions(operationType=OperationTypeEnum.IMAGE_GENERATE)
# Calculate input bytes # Calculate input bytes
inputBytes = len(prompt.encode("utf-8")) inputBytes = len(prompt.encode("utf-8"))
@ -315,8 +365,8 @@ class AiObjects:
outputBytes = len(content.encode("utf-8")) outputBytes = len(content.encode("utf-8"))
# Calculate price using model's cost information # Calculate price using model's cost information
estimated_tokens = inputBytes / 4 estimatedTokens = inputBytes / 4
priceUsd = (estimated_tokens / 1000) * selectedModel.costPer1kTokens + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput priceUsd = (estimatedTokens / 1000) * selectedModel.costPer1kTokensInput + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
logger.info(f"✅ Image generation successful with model: {modelName}") logger.info(f"✅ Image generation successful with model: {modelName}")
return AiCallResponse( return AiCallResponse(
@ -343,30 +393,30 @@ class AiObjects:
) )
# Web functionality methods - Simple interface to Tavily connector # Web functionality methods - Simple interface to Tavily connector
async def search_websites(self, query: str, max_results: int = 5, **kwargs) -> List[WebSearchResultItem]: async def searchWebsites(self, query: str, maxResults: int = 5, **kwargs) -> List[WebSearchResultItem]:
"""Search for websites using Tavily.""" """Search for websites using Tavily."""
request = WebSearchRequest( request = WebSearchRequest(
query=query, query=query,
max_results=max_results, max_results=maxResults,
**kwargs **kwargs
) )
# Get Tavily connector from registry # Get Tavily connector from registry
tavily_connector = model_registry.getConnectorForModel("tavily_search") tavilyConnector = model_registry.getConnectorForModel("tavily_search")
if not tavily_connector: if not tavilyConnector:
raise ValueError("Tavily connector not available") raise ValueError("Tavily connector not available")
result = await tavily_connector.search(request) result = await tavilyConnector.search(request)
if result.success and result.documents: if result.success and result.documents:
return result.documents[0].documentData.results return result.documents[0].documentData.results
return [] return []
async def crawl_websites(self, urls: List[str], extract_depth: str = "advanced", format: str = "markdown") -> List[WebCrawlResultItem]: async def crawlWebsites(self, urls: List[str], extractDepth: str = "advanced", format: str = "markdown") -> List[WebCrawlResultItem]:
"""Crawl websites using Tavily.""" """Crawl websites using Tavily."""
from pydantic import HttpUrl from pydantic import HttpUrl
from urllib.parse import urlparse from urllib.parse import urlparse
# Safely create HttpUrl objects with proper scheme handling # Safely create HttpUrl objects with proper scheme handling
http_urls = [] httpUrls = []
for url in urls: for url in urls:
try: try:
# Ensure URL has a scheme # Ensure URL has a scheme
@ -375,44 +425,44 @@ class AiObjects:
url = f"https://{url}" url = f"https://{url}"
# Use HttpUrl with scheme parameter (this works for all URLs) # Use HttpUrl with scheme parameter (this works for all URLs)
http_urls.append(HttpUrl(url, scheme="https")) httpUrls.append(HttpUrl(url, scheme="https"))
except Exception as e: except Exception as e:
logger.warning(f"Skipping invalid URL {url}: {e}") logger.warning(f"Skipping invalid URL {url}: {e}")
continue continue
if not http_urls: if not httpUrls:
return [] return []
request = WebCrawlRequest( request = WebCrawlRequest(
urls=http_urls, urls=httpUrls,
extract_depth=extract_depth, extract_depth=extractDepth,
format=format format=format
) )
# Get Tavily connector from registry # Get Tavily connector from registry
tavily_connector = model_registry.getConnectorForModel("tavily_crawl") tavilyConnector = model_registry.getConnectorForModel("tavily_crawl")
if not tavily_connector: if not tavilyConnector:
raise ValueError("Tavily connector not available") raise ValueError("Tavily connector not available")
result = await tavily_connector.crawl(request) result = await tavilyConnector.crawl(request)
if result.success and result.documents: if result.success and result.documents:
return result.documents[0].documentData.results return result.documents[0].documentData.results
return [] return []
async def extract_content(self, urls: List[str], extract_depth: str = "advanced", format: str = "markdown") -> Dict[str, str]: async def extractContent(self, urls: List[str], extractDepth: str = "advanced", format: str = "markdown") -> Dict[str, str]:
"""Extract content from URLs and return as dictionary.""" """Extract content from URLs and return as dictionary."""
crawl_results = await self.crawl_websites(urls, extract_depth, format) crawlResults = await self.crawlWebsites(urls, extractDepth, format)
return {str(result.url): result.content for result in crawl_results} return {str(result.url): result.content for result in crawlResults}
# Core Web Tools - Clean interface for web operations # Core Web Tools - Clean interface for web operations
async def readPage(self, url: str, extract_depth: str = "advanced") -> Optional[str]: async def readPage(self, url: str, extractDepth: str = "advanced") -> Optional[str]:
"""Read a single web page and return its content (HTML/Markdown).""" """Read a single web page and return its content (HTML/Markdown)."""
logger.debug(f"Reading page: {url}") logger.debug(f"Reading page: {url}")
try: try:
# URL encode the URL to handle spaces and special characters # URL encode the URL to handle spaces and special characters
from urllib.parse import quote, urlparse, urlunparse from urllib.parse import quote, urlparse, urlunparse
parsed = urlparse(url) parsed = urlparse(url)
encoded_url = urlunparse(( encodedUrl = urlunparse((
parsed.scheme, parsed.scheme,
parsed.netloc, parsed.netloc,
parsed.path, parsed.path,
@ -423,53 +473,53 @@ class AiObjects:
# Manually encode query parameters to handle spaces # Manually encode query parameters to handle spaces
if parsed.query: if parsed.query:
encoded_query = quote(parsed.query, safe='=&') encodedQuery = quote(parsed.query, safe='=&')
encoded_url = urlunparse(( encodedUrl = urlunparse((
parsed.scheme, parsed.scheme,
parsed.netloc, parsed.netloc,
parsed.path, parsed.path,
parsed.params, parsed.params,
encoded_query, encodedQuery,
parsed.fragment parsed.fragment
)) ))
logger.debug(f"URL encoded: {url} -> {encoded_url}") logger.debug(f"URL encoded: {url} -> {encodedUrl}")
content = await self.extract_content([encoded_url], extract_depth, "markdown") content = await self.extractContent([encodedUrl], extractDepth, "markdown")
result = content.get(encoded_url) result = content.get(encodedUrl)
if result: if result:
logger.debug(f"Successfully read page {encoded_url}: {len(result)} chars") logger.debug(f"Successfully read page {encodedUrl}: {len(result)} chars")
else: else:
logger.warning(f"No content returned for page {encoded_url}") logger.warning(f"No content returned for page {encodedUrl}")
return result return result
except Exception as e: except Exception as e:
logger.warning(f"Failed to read page {url}: {e}") logger.warning(f"Failed to read page {url}: {e}")
return None return None
async def getUrlsFromPage(self, url: str, extract_depth: str = "advanced") -> List[str]: async def getUrlsFromPage(self, url: str, extractDepth: str = "advanced") -> List[str]:
"""Get all URLs from a web page, with redundancies removed.""" """Get all URLs from a web page, with redundancies removed."""
try: try:
content = await self.readPage(url, extract_depth) content = await self.readPage(url, extractDepth)
if not content: if not content:
return [] return []
links = self._extractLinksFromContent(content, url) links = self._extractLinksFromContent(content, url)
# Remove duplicates while preserving order # Remove duplicates while preserving order
seen = set() seen = set()
unique_links = [] uniqueLinks = []
for link in links: for link in links:
if link not in seen: if link not in seen:
seen.add(link) seen.add(link)
unique_links.append(link) uniqueLinks.append(link)
logger.debug(f"Extracted {len(unique_links)} unique URLs from {url}") logger.debug(f"Extracted {len(uniqueLinks)} unique URLs from {url}")
return unique_links return uniqueLinks
except Exception as e: except Exception as e:
logger.warning(f"Failed to get URLs from page {url}: {e}") logger.warning(f"Failed to get URLs from page {url}: {e}")
return [] return []
def filterUrlsOnlyPages(self, urls: List[str], max_per_domain: int = 10) -> List[str]: def filterUrlsOnlyPages(self, urls: List[str], maxPerDomain: int = 10) -> List[str]:
"""Filter URLs to get only links for pages to follow (no images, etc.).""" """Filter URLs to get only links for pages to follow (no images, etc.)."""
from urllib.parse import urlparse from urllib.parse import urlparse
@ -482,35 +532,35 @@ class AiObjects:
return not lower.endswith(blocked) return not lower.endswith(blocked)
# Group by domain # Group by domain
domain_links = {} domainLinks = {}
for link in urls: for link in urls:
domain = urlparse(link).netloc domain = urlparse(link).netloc
if domain not in domain_links: if domain not in domainLinks:
domain_links[domain] = [] domainLinks[domain] = []
domain_links[domain].append(link) domainLinks[domain].append(link)
# Filter and cap per domain # Filter and cap per domain
filtered_links = [] filteredLinks = []
for domain, domain_link_list in domain_links.items(): for domain, domainLinkList in domainLinks.items():
seen = set() seen = set()
domain_filtered = [] domainFiltered = []
for link in domain_link_list: for link in domainLinkList:
if link in seen: if link in seen:
continue continue
if not _isHtmlCandidate(link): if not _isHtmlCandidate(link):
continue continue
seen.add(link) seen.add(link)
domain_filtered.append(link) domainFiltered.append(link)
if len(domain_filtered) >= max_per_domain: if len(domainFiltered) >= maxPerDomain:
break break
filtered_links.extend(domain_filtered) filteredLinks.extend(domainFiltered)
logger.debug(f"Domain {domain}: {len(domain_link_list)} -> {len(domain_filtered)} links") logger.debug(f"Domain {domain}: {len(domainLinkList)} -> {len(domainFiltered)} links")
return filtered_links return filteredLinks
def _extractLinksFromContent(self, content: str, base_url: str) -> List[str]: def _extractLinksFromContent(self, content: str, baseUrl: str) -> List[str]:
"""Extract links from HTML/Markdown content.""" """Extract links from HTML/Markdown content."""
try: try:
import re import re
@ -523,19 +573,19 @@ class AiObjects:
# If it's a relative URL, make it absolute first # If it's a relative URL, make it absolute first
if not url.startswith(('http://', 'https://')): if not url.startswith(('http://', 'https://')):
url = urljoin(base_url, url) url = urljoin(baseUrl, url)
# Parse and re-encode the URL properly # Parse and re-encode the URL properly
parsed = urlparse(url) parsed = urlparse(url)
if parsed.query: if parsed.query:
# Encode query parameters properly # Encode query parameters properly
encoded_query = quote(parsed.query, safe='=&') encodedQuery = quote(parsed.query, safe='=&')
url = urlunparse(( url = urlunparse((
parsed.scheme, parsed.scheme,
parsed.netloc, parsed.netloc,
parsed.path, parsed.path,
parsed.params, parsed.params,
encoded_query, encodedQuery,
parsed.fragment parsed.fragment
)) ))
@ -544,45 +594,45 @@ class AiObjects:
links = [] links = []
# Extract HTML links: <a href="url"> format # Extract HTML links: <a href="url"> format
html_link_pattern = r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>' htmlLinkPattern = r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>'
html_links = re.findall(html_link_pattern, content, re.IGNORECASE) htmlLinks = re.findall(htmlLinkPattern, content, re.IGNORECASE)
for url in html_links: for url in htmlLinks:
if url and not url.startswith('#') and not url.startswith('javascript:'): if url and not url.startswith('#') and not url.startswith('javascript:'):
try: try:
cleaned_url = _cleanUrl(url) cleanedUrl = _cleanUrl(url)
links.append(cleaned_url) links.append(cleanedUrl)
logger.debug(f"Extracted HTML link: {url} -> {cleaned_url}") logger.debug(f"Extracted HTML link: {url} -> {cleanedUrl}")
except Exception as e: except Exception as e:
logger.debug(f"Failed to clean HTML link {url}: {e}") logger.debug(f"Failed to clean HTML link {url}: {e}")
# Extract markdown links: [text](url) format # Extract markdown links: [text](url) format
markdown_link_pattern = r'\[([^\]]+)\]\(([^)]+)\)' markdownLinkPattern = r'\[([^\]]+)\]\(([^)]+)\)'
markdown_links = re.findall(markdown_link_pattern, content) markdownLinks = re.findall(markdownLinkPattern, content)
for text, url in markdown_links: for text, url in markdownLinks:
if url and not url.startswith('#'): if url and not url.startswith('#'):
try: try:
cleaned_url = _cleanUrl(url) cleanedUrl = _cleanUrl(url)
# Only keep URLs from the same domain # Only keep URLs from the same domain
if urlparse(cleaned_url).netloc == urlparse(base_url).netloc: if urlparse(cleanedUrl).netloc == urlparse(baseUrl).netloc:
links.append(cleaned_url) links.append(cleanedUrl)
logger.debug(f"Extracted markdown link: {url} -> {cleaned_url}") logger.debug(f"Extracted markdown link: {url} -> {cleanedUrl}")
except Exception as e: except Exception as e:
logger.debug(f"Failed to clean markdown link {url}: {e}") logger.debug(f"Failed to clean markdown link {url}: {e}")
# Extract plain URLs in the text # Extract plain URLs in the text
url_pattern = r'https?://[^\s\)]+' urlPattern = r'https?://[^\s\)]+'
plain_urls = re.findall(url_pattern, content) plainUrls = re.findall(urlPattern, content)
for url in plain_urls: for url in plainUrls:
try: try:
clean_url = url.rstrip('.,;!?') cleanUrl = url.rstrip('.,;!?')
cleaned_url = _cleanUrl(clean_url) cleanedUrl = _cleanUrl(cleanUrl)
if urlparse(cleaned_url).netloc == urlparse(base_url).netloc: if urlparse(cleanedUrl).netloc == urlparse(baseUrl).netloc:
if cleaned_url not in links: # Avoid duplicates if cleanedUrl not in links: # Avoid duplicates
links.append(cleaned_url) links.append(cleanedUrl)
logger.debug(f"Extracted plain URL: {url} -> {cleaned_url}") logger.debug(f"Extracted plain URL: {url} -> {cleanedUrl}")
except Exception as e: except Exception as e:
logger.debug(f"Failed to clean plain URL {url}: {e}") logger.debug(f"Failed to clean plain URL {url}: {e}")
@ -716,7 +766,7 @@ class AiObjects:
"""Use Perplexity AI to provide the best answers for web-related queries.""" """Use Perplexity AI to provide the best answers for web-related queries."""
if options is None: if options is None:
options = AiCallOptions(operationType=OperationType.WEB_RESEARCH) options = AiCallOptions(operationType=OperationTypeEnum.WEB_RESEARCH)
# Calculate input bytes # Calculate input bytes
inputBytes = len((query + context).encode("utf-8")) inputBytes = len((query + context).encode("utf-8"))
@ -756,7 +806,7 @@ Format your response in a clear, professional manner that would be helpful for s
perplexity_model = model_registry.getModel("perplexity_callAiWithWebSearch") perplexity_model = model_registry.getModel("perplexity_callAiWithWebSearch")
if perplexity_model: if perplexity_model:
estimated_tokens = inputBytes / 4 estimated_tokens = inputBytes / 4
priceUsd = (estimated_tokens / 1000) * perplexity_model.costPer1kTokens + (outputBytes / 4 / 1000) * perplexity_model.costPer1kTokensOutput priceUsd = (estimated_tokens / 1000) * perplexity_model.costPer1kTokensInput + (outputBytes / 4 / 1000) * perplexity_model.costPer1kTokensOutput
else: else:
priceUsd = 0.0 priceUsd = 0.0

View file

@ -1,11 +1,8 @@
import logging import logging
import re
from typing import Dict, Any, List, Optional, Tuple, Union from typing import Dict, Any, List, Optional, Tuple, Union
from modules.datamodels.datamodelChat import PromptPlaceholder from modules.datamodels.datamodelChat import PromptPlaceholder, ChatDocument
from modules.datamodels.datamodelChat import ChatDocument
from modules.services.serviceExtraction.mainServiceExtraction import ExtractionService from modules.services.serviceExtraction.mainServiceExtraction import ExtractionService
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, ModelCapabilities, OperationType, Priority from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum
from modules.datamodels.datamodelExtraction import ChunkResult, ContentExtracted from modules.datamodels.datamodelExtraction import ChunkResult, ContentExtracted
from modules.datamodels.datamodelWeb import ( from modules.datamodels.datamodelWeb import (
WebResearchRequest, WebResearchRequest,
@ -19,6 +16,7 @@ from modules.services.serviceAi.subCoreAi import SubCoreAi
from modules.services.serviceAi.subDocumentProcessing import SubDocumentProcessing from modules.services.serviceAi.subDocumentProcessing import SubDocumentProcessing
from modules.services.serviceAi.subWebResearch import SubWebResearch from modules.services.serviceAi.subWebResearch import SubWebResearch
from modules.services.serviceAi.subDocumentGeneration import SubDocumentGeneration from modules.services.serviceAi.subDocumentGeneration import SubDocumentGeneration
from modules.services.serviceAi.subSharedAiUtils import sanitizePromptContent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -170,68 +168,3 @@ class AiService:
return await self.coreAi.callAiDocuments(prompt, documents, options, outputFormat, title, "json") return await self.coreAi.callAiDocuments(prompt, documents, options, outputFormat, title, "json")
def sanitizePromptContent(self, content: str, contentType: str = "text") -> str:
"""
Centralized prompt content sanitization to prevent injection attacks and ensure safe presentation.
This is the single source of truth for all prompt sanitization across the system.
Replaces all scattered sanitization functions with a unified approach.
Args:
content: The content to sanitize
contentType: Type of content ("text", "userinput", "json", "document")
Returns:
Safely sanitized content ready for AI prompt insertion
"""
if not content:
return ""
try:
# Convert to string if not already
content_str = str(content)
# Remove null bytes and control characters (except newlines and tabs)
sanitized = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', content_str)
# Handle different content types with appropriate sanitization
if contentType == "userinput":
# Extra security for user-controlled content
# Escape curly braces to prevent placeholder injection
sanitized = sanitized.replace('{', '{{').replace('}', '}}')
# Escape quotes and wrap in single quotes
sanitized = sanitized.replace('"', '\\"').replace("'", "\\'")
return f"'{sanitized}'"
elif contentType == "json":
# For JSON content, escape quotes and backslashes
sanitized = sanitized.replace('\\', '\\\\')
sanitized = sanitized.replace('"', '\\"')
sanitized = sanitized.replace('\n', '\\n')
sanitized = sanitized.replace('\r', '\\r')
sanitized = sanitized.replace('\t', '\\t')
elif contentType == "document":
# For document content, escape special characters
sanitized = sanitized.replace('\\', '\\\\')
sanitized = sanitized.replace('"', '\\"')
sanitized = sanitized.replace("'", "\\'")
sanitized = sanitized.replace('\n', '\\n')
sanitized = sanitized.replace('\r', '\\r')
sanitized = sanitized.replace('\t', '\\t')
else: # contentType == "text" or default
# Basic text sanitization
sanitized = sanitized.replace('\\', '\\\\')
sanitized = sanitized.replace('"', '\\"')
sanitized = sanitized.replace("'", "\\'")
sanitized = sanitized.replace('\n', '\\n')
sanitized = sanitized.replace('\r', '\\r')
sanitized = sanitized.replace('\t', '\\t')
return sanitized
except Exception as e:
logger.error(f"Error sanitizing prompt content: {str(e)}")
# Return a safe fallback
return "[ERROR: Content could not be safely sanitized]"

View file

@ -2,7 +2,13 @@ import json
import logging import logging
from typing import Dict, Any, List, Optional, Tuple, Union from typing import Dict, Any, List, Optional, Tuple, Union
from modules.datamodels.datamodelChat import PromptPlaceholder, ChatDocument from modules.datamodels.datamodelChat import PromptPlaceholder, ChatDocument
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, ModelCapabilities, OperationType, Priority from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum
from modules.services.serviceAi.subSharedAiUtils import (
buildPromptWithPlaceholders,
extractTextFromContentParts,
reduceText,
determineCallType
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -289,23 +295,6 @@ CRITICAL REQUIREMENTS:
logger.error(f"Error merging JSON content: {str(e)}") logger.error(f"Error merging JSON content: {str(e)}")
return accumulatedContent[0] # Return first response on error return accumulatedContent[0] # Return first response on error
def _buildPromptWithPlaceholders(self, prompt: str, placeholders: Optional[Dict[str, str]]) -> str:
"""
Build full prompt by replacing placeholders with their content.
Uses the new {{KEY:placeholder}} format.
"""
if not placeholders:
return prompt
full_prompt = prompt
for placeholder, content in placeholders.items():
# Replace both old format {{placeholder}} and new format {{KEY:placeholder}}
full_prompt = full_prompt.replace(f"{{{{{placeholder}}}}}", content)
full_prompt = full_prompt.replace(f"{{{{KEY:{placeholder}}}}}", content)
return full_prompt
async def _buildGenerationPrompt( async def _buildGenerationPrompt(
self, self,
prompt: str, prompt: str,
@ -359,12 +348,12 @@ CRITICAL REQUIREMENTS:
# Build full prompt with placeholders # Build full prompt with placeholders
if placeholders: if placeholders:
placeholders_dict = {p.label: p.content for p in placeholders} placeholders_dict = {p.label: p.content for p in placeholders}
full_prompt = self._buildPromptWithPlaceholders(prompt, placeholders_dict) full_prompt = buildPromptWithPlaceholders(prompt, placeholders_dict)
else: else:
full_prompt = prompt full_prompt = prompt
# Use shared core function with planning-specific debug prefix # Use shared core function with planning-specific debug prefix
return await self._callAiWithLooping(full_prompt, options, "planning", loopInstructionFormat=loopInstructionFormat) return await self._callAiWithLooping(full_prompt, options, "plan", loopInstructionFormat=loopInstructionFormat)
# Document Generation AI Call # Document Generation AI Call
async def callAiDocuments( async def callAiDocuments(
@ -485,12 +474,12 @@ CRITICAL REQUIREMENTS:
self.services.utils.debugLogToFile(f"readImage called with prompt, imageData type: {type(imageData)}, length: {len(imageData) if imageData else 0}, mimeType: {mimeType}", "AI_SERVICE") self.services.utils.debugLogToFile(f"readImage called with prompt, imageData type: {type(imageData)}, length: {len(imageData) if imageData else 0}, mimeType: {mimeType}", "AI_SERVICE")
logger.info(f"readImage called with prompt, imageData type: {type(imageData)}, length: {len(imageData) if imageData else 0}, mimeType: {mimeType}") logger.info(f"readImage called with prompt, imageData type: {type(imageData)}, length: {len(imageData) if imageData else 0}, mimeType: {mimeType}")
# Always use IMAGE_ANALYSIS operation type for image processing # Always use IMAGE_ANALYSE operation type for image processing
if options is None: if options is None:
options = AiCallOptions(operationType=OperationType.IMAGE_ANALYSIS) options = AiCallOptions(operationType=OperationTypeEnum.IMAGE_ANALYSE)
else: else:
# Override the operation type to ensure image analysis # Override the operation type to ensure image analysis
options.operationType = OperationType.IMAGE_ANALYSIS options.operationType = OperationTypeEnum.IMAGE_ANALYSE
self.services.utils.debugLogToFile(f"Calling aiObjects.callImage with operationType: {options.operationType}", "AI_SERVICE") self.services.utils.debugLogToFile(f"Calling aiObjects.callImage with operationType: {options.operationType}", "AI_SERVICE")
logger.info(f"Calling aiObjects.callImage with operationType: {options.operationType}") logger.info(f"Calling aiObjects.callImage with operationType: {options.operationType}")
@ -559,20 +548,6 @@ CRITICAL REQUIREMENTS:
logger.error(f"Error in AI image generation: {str(e)}") logger.error(f"Error in AI image generation: {str(e)}")
return {"success": False, "error": str(e)} return {"success": False, "error": str(e)}
def _determineCallType(self, documents: Optional[List[ChatDocument]], operation_type: str) -> str:
"""
Determine call type based on documents and operation type.
Criteria: no documents AND operationType is "generate_plan" -> planning
All other cases -> text
"""
has_documents = documents is not None and len(documents) > 0
is_planning_operation = operation_type == OperationType.GENERATE_PLAN
if not has_documents and is_planning_operation:
return "planning"
else:
return "text"
def _getModelCapabilitiesForContent(self, prompt: str, documents: Optional[List[ChatDocument]], options: AiCallOptions) -> Dict[str, int]: def _getModelCapabilitiesForContent(self, prompt: str, documents: Optional[List[ChatDocument]], options: AiCallOptions) -> Dict[str, int]:
@ -606,11 +581,11 @@ CRITICAL REQUIREMENTS:
# Check if model supports the operation type # Check if model supports the operation type
capabilities = model_info.get("capabilities", []) capabilities = model_info.get("capabilities", [])
if options.operationType == OperationType.IMAGE_ANALYSIS and "image_analysis" not in capabilities: if options.operationType == OperationTypeEnum.IMAGE_ANALYSE and "imageAnalyse" not in capabilities:
continue continue
elif options.operationType == OperationType.IMAGE_GENERATION and "image_generation" not in capabilities: elif options.operationType == OperationTypeEnum.IMAGE_GENERATE and "imageGenerate" not in capabilities:
continue continue
elif options.operationType == OperationType.WEB_RESEARCH and "web_search" not in capabilities: elif options.operationType == OperationTypeEnum.WEB_RESEARCH and "web_search" not in capabilities:
continue continue
elif "text_generation" not in capabilities: elif "text_generation" not in capabilities:
continue continue
@ -649,68 +624,4 @@ CRITICAL REQUIREMENTS:
"imageChunkSize": image_chunk_size "imageChunkSize": image_chunk_size
} }
def _buildPromptWithPlaceholders(self, prompt: str, placeholders: Optional[Dict[str, str]]) -> str:
"""
Build full prompt by replacing placeholders with their content.
Uses the new {{KEY:placeholder}} format.
"""
if not placeholders:
return prompt
full_prompt = prompt
for placeholder, content in placeholders.items():
# Replace both old format {{placeholder}} and new format {{KEY:placeholder}}
full_prompt = full_prompt.replace(f"{{{{{placeholder}}}}}", content)
full_prompt = full_prompt.replace(f"{{{{KEY:{placeholder}}}}}", content)
return full_prompt
def _reducePlanningPrompt(
self,
full_prompt: str,
placeholders: Optional[Dict[str, str]],
model: ModelCapabilities,
options: AiCallOptions
) -> str:
"""
Reduce planning prompt size by summarizing placeholders while preserving prompt structure.
"""
if not placeholders:
return self._reduceText(full_prompt, 0.7)
# Reduce placeholders while preserving prompt
reduced_placeholders = {}
for placeholder, content in placeholders.items():
if len(content) > 1000: # Only reduce long content
reduction_factor = 0.7
reduced_content = self._reduceText(content, reduction_factor)
reduced_placeholders[placeholder] = reduced_content
else:
reduced_placeholders[placeholder] = content
return self._buildPromptWithPlaceholders(full_prompt, reduced_placeholders)
def _extractTextFromContentParts(self, extracted_content) -> str:
"""
Extract text content from ExtractionService ContentPart objects.
"""
if not extracted_content or not hasattr(extracted_content, 'parts'):
return ""
text_parts = []
for part in extracted_content.parts:
if hasattr(part, 'typeGroup') and part.typeGroup in ['text', 'table', 'structure']:
if hasattr(part, 'data') and part.data:
text_parts.append(part.data)
return "\n\n".join(text_parts)
def _reduceText(self, text: str, reduction_factor: float) -> str:
"""
Reduce text size by the specified factor.
"""
if reduction_factor >= 1.0:
return text
target_length = int(len(text) * reduction_factor)
return text[:target_length] + "... [reduced]"

View file

@ -5,7 +5,7 @@ import time
from datetime import datetime, UTC from datetime import datetime, UTC
from typing import Dict, Any, List, Optional, Tuple, Union from typing import Dict, Any, List, Optional, Tuple, Union
from modules.datamodels.datamodelChat import ChatDocument from modules.datamodels.datamodelChat import ChatDocument
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -335,9 +335,9 @@ class SubDocumentGeneration:
) )
# Prepare the AI call # Prepare the AI call
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
requestOptions = AiCallOptions() requestOptions = AiCallOptions()
requestOptions.operationType = OperationType.GENERAL requestOptions.operationType = OperationTypeEnum.GENERAL
# Create context with the extracted JSON content # Create context with the extracted JSON content
context = f"Extracted JSON content:\n{json.dumps(docData, indent=2)}" context = f"Extracted JSON content:\n{json.dumps(docData, indent=2)}"
@ -477,9 +477,9 @@ Consider:
Return only the JSON response. Return only the JSON response.
""" """
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
request_options = AiCallOptions() request_options = AiCallOptions()
request_options.operationType = OperationType.GENERAL request_options.operationType = OperationTypeEnum.GENERAL
request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options) request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options)
response = await ai_service.aiObjects.call(request) response = await ai_service.aiObjects.call(request)

View file

@ -4,7 +4,7 @@ import re
import time import time
from typing import Dict, Any, List, Optional, Tuple, Union from typing import Dict, Any, List, Optional, Tuple, Union
from modules.datamodels.datamodelChat import ChatDocument from modules.datamodels.datamodelChat import ChatDocument
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, ModelCapabilities, OperationType, Priority from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum
from modules.datamodels.datamodelExtraction import ChunkResult, ContentExtracted from modules.datamodels.datamodelExtraction import ChunkResult, ContentExtracted
from modules.services.serviceExtraction.mainServiceExtraction import ExtractionService from modules.services.serviceExtraction.mainServiceExtraction import ExtractionService
@ -84,7 +84,7 @@ class SubDocumentProcessing:
"imageQuality": 85, "imageQuality": 85,
"mergeStrategy": { "mergeStrategy": {
"useIntelligentMerging": True, # Enable intelligent token-aware merging "useIntelligentMerging": True, # Enable intelligent token-aware merging
"modelCapabilities": model_capabilities, "capabilities": model_capabilities,
"prompt": prompt, "prompt": prompt,
"groupBy": "typeGroup", "groupBy": "typeGroup",
"orderBy": "id", "orderBy": "id",
@ -145,7 +145,7 @@ class SubDocumentProcessing:
"imageQuality": 85, "imageQuality": 85,
"mergeStrategy": { "mergeStrategy": {
"useIntelligentMerging": True, # Enable intelligent token-aware merging "useIntelligentMerging": True, # Enable intelligent token-aware merging
"modelCapabilities": model_capabilities, "capabilities": model_capabilities,
"prompt": prompt, "prompt": prompt,
"groupBy": "typeGroup", "groupBy": "typeGroup",
"orderBy": "id", "orderBy": "id",
@ -240,7 +240,7 @@ class SubDocumentProcessing:
"imageQuality": 85, "imageQuality": 85,
"mergeStrategy": { "mergeStrategy": {
"useIntelligentMerging": True, # Enable intelligent token-aware merging "useIntelligentMerging": True, # Enable intelligent token-aware merging
"modelCapabilities": model_capabilities, "capabilities": model_capabilities,
"prompt": custom_prompt, # Use the custom prompt "prompt": custom_prompt, # Use the custom prompt
"groupBy": "typeGroup", "groupBy": "typeGroup",
"orderBy": "id", "orderBy": "id",
@ -666,7 +666,7 @@ CONTINUATION INSTRUCTIONS:
elif part.mimeType and part.data and len(part.data.strip()) > 0: elif part.mimeType and part.data and len(part.data.strip()) > 0:
# Process any document container as text content # Process any document container as text content
request_options = options if options is not None else AiCallOptions() request_options = options if options is not None else AiCallOptions()
request_options.operationType = OperationType.GENERAL request_options.operationType = OperationTypeEnum.GENERAL
self.services.utils.debugLogToFile(f"EXTRACTION CONTAINER CHUNK {chunk_index}: Processing {part.mimeType} container as text with generate_json={generate_json}", "AI_SERVICE") self.services.utils.debugLogToFile(f"EXTRACTION CONTAINER CHUNK {chunk_index}: Processing {part.mimeType} container as text with generate_json={generate_json}", "AI_SERVICE")
logger.info(f"Chunk {chunk_index}: Processing {part.mimeType} container as text with generate_json={generate_json}") logger.info(f"Chunk {chunk_index}: Processing {part.mimeType} container as text with generate_json={generate_json}")
@ -755,7 +755,7 @@ CONTINUATION INSTRUCTIONS:
# Ensure options is not None and set correct operation type for text # Ensure options is not None and set correct operation type for text
request_options = options if options is not None else AiCallOptions() request_options = options if options is not None else AiCallOptions()
# FIXED: Set operation type to general for text processing # FIXED: Set operation type to general for text processing
request_options.operationType = OperationType.GENERAL request_options.operationType = OperationTypeEnum.GENERAL
self.services.utils.debugLogToFile(f"EXTRACTION CHUNK {chunk_index}: Calling aiObjects.call with operationType={request_options.operationType}, generate_json={generate_json}", "AI_SERVICE") self.services.utils.debugLogToFile(f"EXTRACTION CHUNK {chunk_index}: Calling aiObjects.call with operationType={request_options.operationType}, generate_json={generate_json}", "AI_SERVICE")
logger.info(f"Chunk {chunk_index}: Calling aiObjects.call with operationType={request_options.operationType}, generate_json={generate_json}") logger.info(f"Chunk {chunk_index}: Calling aiObjects.call with operationType={request_options.operationType}, generate_json={generate_json}")
@ -1222,11 +1222,11 @@ CONTINUATION INSTRUCTIONS:
# Check if model supports the operation type # Check if model supports the operation type
capabilities = model_info.get("capabilities", []) capabilities = model_info.get("capabilities", [])
if options.operationType == OperationType.IMAGE_ANALYSIS and "image_analysis" not in capabilities: if options.operationType == OperationTypeEnum.IMAGE_ANALYSE and "imageAnalyse" not in capabilities:
continue continue
elif options.operationType == OperationType.IMAGE_GENERATION and "image_generation" not in capabilities: elif options.operationType == OperationTypeEnum.IMAGE_GENERATE and "imageGenerate" not in capabilities:
continue continue
elif options.operationType == OperationType.WEB_RESEARCH and "web_search" not in capabilities: elif options.operationType == OperationTypeEnum.WEB_RESEARCH and "web_search" not in capabilities:
continue continue
elif "text_generation" not in capabilities: elif "text_generation" not in capabilities:
continue continue

View file

@ -0,0 +1,164 @@
"""
Shared utilities for AI services to eliminate code duplication.
This module contains common functions used across multiple AI service modules
to maintain DRY principles and ensure consistency.
"""
import re
import logging
from typing import Dict, Any, List, Optional, Union
from modules.datamodels.datamodelChat import PromptPlaceholder
logger = logging.getLogger(__name__)
def buildPromptWithPlaceholders(prompt: str, placeholders: Optional[Dict[str, str]]) -> str:
"""
Build full prompt by replacing placeholders with their content.
Uses the new {{KEY:placeholder}} format.
Args:
prompt: The base prompt template
placeholders: Dictionary of placeholder key-value pairs
Returns:
Prompt with placeholders replaced
"""
if not placeholders:
return prompt
full_prompt = prompt
for placeholder, content in placeholders.items():
# Replace both old format {{placeholder}} and new format {{KEY:placeholder}}
full_prompt = full_prompt.replace(f"{{{{{placeholder}}}}}", content)
full_prompt = full_prompt.replace(f"{{{{KEY:{placeholder}}}}}", content)
return full_prompt
def sanitizePromptContent(content: str, contentType: str = "text") -> str:
"""
Centralized prompt content sanitization to prevent injection attacks and ensure safe presentation.
This is the single source of truth for all prompt sanitization across the system.
Replaces all scattered sanitization functions with a unified approach.
Args:
content: The content to sanitize
contentType: Type of content ("text", "userinput", "json", "document")
Returns:
Safely sanitized content ready for AI prompt insertion
"""
if not content:
return ""
try:
# Convert to string if not already
content_str = str(content)
# Remove null bytes and control characters (except newlines and tabs)
sanitized = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', content_str)
# Handle different content types with appropriate sanitization
if contentType == "userinput":
# Extra security for user-controlled content
# Escape curly braces to prevent placeholder injection
sanitized = sanitized.replace('{', '{{').replace('}', '}}')
# Escape quotes and wrap in single quotes
sanitized = sanitized.replace('"', '\\"').replace("'", "\\'")
return f"'{sanitized}'"
elif contentType == "json":
# For JSON content, escape quotes and backslashes
sanitized = sanitized.replace('\\', '\\\\')
sanitized = sanitized.replace('"', '\\"')
sanitized = sanitized.replace('\n', '\\n')
sanitized = sanitized.replace('\r', '\\r')
sanitized = sanitized.replace('\t', '\\t')
elif contentType == "document":
# For document content, escape special characters
sanitized = sanitized.replace('\\', '\\\\')
sanitized = sanitized.replace('"', '\\"')
sanitized = sanitized.replace("'", "\\'")
sanitized = sanitized.replace('\n', '\\n')
sanitized = sanitized.replace('\r', '\\r')
sanitized = sanitized.replace('\t', '\\t')
else: # contentType == "text" or default
# Basic text sanitization
sanitized = sanitized.replace('\\', '\\\\')
sanitized = sanitized.replace('"', '\\"')
sanitized = sanitized.replace("'", "\\'")
sanitized = sanitized.replace('\n', '\\n')
sanitized = sanitized.replace('\r', '\\r')
sanitized = sanitized.replace('\t', '\\t')
return sanitized
except Exception as e:
logger.error(f"Error sanitizing prompt content: {str(e)}")
# Return a safe fallback
return "[ERROR: Content could not be safely sanitized]"
def extractTextFromContentParts(extracted_content) -> str:
"""
Extract text content from ExtractionService ContentPart objects.
Args:
extracted_content: ContentExtracted object with parts
Returns:
Concatenated text content from all text/table/structure parts
"""
if not extracted_content or not hasattr(extracted_content, 'parts'):
return ""
text_parts = []
for part in extracted_content.parts:
if hasattr(part, 'typeGroup') and part.typeGroup in ['text', 'table', 'structure']:
if hasattr(part, 'data') and part.data:
text_parts.append(part.data)
return "\n\n".join(text_parts)
def reduceText(text: str, reduction_factor: float) -> str:
"""
Reduce text size by the specified factor.
Args:
text: Text to reduce
reduction_factor: Factor by which to reduce (0.0 to 1.0)
Returns:
Reduced text with truncation indicator
"""
if reduction_factor >= 1.0:
return text
target_length = int(len(text) * reduction_factor)
return text[:target_length] + "... [reduced]"
def determineCallType(documents: Optional[List], operation_type: str) -> str:
"""
Determine call type based on documents and operation type.
Args:
documents: List of ChatDocument objects
operation_type: Type of operation being performed
Returns:
Call type: "plan" or "text"
"""
has_documents = documents is not None and len(documents) > 0
is_planning_operation = operation_type == "plan"
if not has_documents and is_planning_operation:
return "plan"
else:
return "text"

View file

@ -368,7 +368,7 @@ class SubWebResearch:
) )
document = WebResearchActionDocument( document = WebResearchActionDocument(
documentName=f"web_research_{request.user_prompt[:50]}.json", documentName=f"webResearch_{request.user_prompt[:50]}.json",
documentData=documentData, documentData=documentData,
mimeType="application/json" mimeType="application/json"
) )
@ -376,7 +376,7 @@ class SubWebResearch:
return WebResearchActionResult( return WebResearchActionResult(
success=True, success=True,
documents=[document], documents=[document],
resultLabel="web_research_results" resultLabel="webResearch_results"
) )
except Exception as e: except Exception as e:

View file

@ -214,7 +214,7 @@ def _applyMerging(parts: List[ContentPart], strategy: Dict[str, Any]) -> List[Co
# Check if intelligent merging is enabled # Check if intelligent merging is enabled
if strategy.get("useIntelligentMerging", False): if strategy.get("useIntelligentMerging", False):
model_capabilities = strategy.get("modelCapabilities", {}) model_capabilities = strategy.get("capabilities", {})
subMerger = IntelligentTokenAwareMerger(model_capabilities) subMerger = IntelligentTokenAwareMerger(model_capabilities)
# Use intelligent merging for all parts # Use intelligent merging for all parts
@ -311,19 +311,19 @@ def applyAiIfRequested(extracted: ContentExtracted, options: Dict[str, Any]) ->
return extracted return extracted
# Placeholder AI processing based on operationType # Placeholder AI processing based on operationType
if operationType == "analyse_content": if operationType == "analyse":
# Add analysis metadata to parts # Add analysis metadata to parts
for part in extracted.parts: for part in extracted.parts:
if part.typeGroup in ("text", "table", "structure"): if part.typeGroup in ("text", "table", "structure"):
part.metadata["ai_processed"] = True part.metadata["ai_processed"] = True
part.metadata["operation_type"] = operationType part.metadata["operation_type"] = operationType
elif operationType == "generate_plan": elif operationType == "plan":
# Add plan generation metadata # Add plan generation metadata
for part in extracted.parts: for part in extracted.parts:
if part.typeGroup == "text": if part.typeGroup == "text":
part.metadata["ai_processed"] = True part.metadata["ai_processed"] = True
part.metadata["operation_type"] = operationType part.metadata["operation_type"] = operationType
elif operationType == "generate_content": elif operationType == "generate":
# Add content generation metadata # Add content generation metadata
for part in extracted.parts: for part in extracted.parts:
part.metadata["ai_processed"] = True part.metadata["ai_processed"] = True

View file

@ -11,7 +11,7 @@ from datetime import datetime, UTC
import base64 import base64
import io import io
from PIL import Image from PIL import Image
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -326,7 +326,7 @@ class BaseRenderer(ABC):
try: try:
request_options = AiCallOptions() request_options = AiCallOptions()
request_options.operationType = OperationType.GENERAL request_options.operationType = OperationTypeEnum.GENERAL
request = AiCallRequest(prompt=style_template, context="", options=request_options) request = AiCallRequest(prompt=style_template, context="", options=request_options)

View file

@ -565,7 +565,7 @@ class RendererDocx(BaseRenderer):
return structure return structure
def _generate_content_from_structure(self, doc, content: str, structure: Dict[str, Any]): def _generate_from_structure(self, doc, content: str, structure: Dict[str, Any]):
"""Generate DOCX content based on extracted structure.""" """Generate DOCX content based on extracted structure."""
# Add sections based on prompt structure # Add sections based on prompt structure
for section in structure['sections']: for section in structure['sections']:

View file

@ -57,7 +57,7 @@ class RendererImage(BaseRenderer):
document_title = extracted_content.get("metadata", {}).get("title", title) document_title = extracted_content.get("metadata", {}).get("title", title)
# Create AI prompt for image generation # Create AI prompt for image generation
image_prompt = await self._create_image_generation_prompt(extracted_content, document_title, user_prompt, ai_service) image_prompt = await self._create_imageGenerate_prompt(extracted_content, document_title, user_prompt, ai_service)
# Save image generation prompt to debug # Save image generation prompt to debug
ai_service.services.utils.writeDebugFile(image_prompt, "rendererImageGenerationPrompt") ai_service.services.utils.writeDebugFile(image_prompt, "rendererImageGenerationPrompt")
@ -88,7 +88,7 @@ class RendererImage(BaseRenderer):
self.logger.error(f"Error generating AI image: {str(e)}") self.logger.error(f"Error generating AI image: {str(e)}")
raise Exception(f"AI image generation failed: {str(e)}") raise Exception(f"AI image generation failed: {str(e)}")
async def _create_image_generation_prompt(self, extracted_content: Dict[str, Any], title: str, user_prompt: str = None, ai_service=None) -> str: async def _create_imageGenerate_prompt(self, extracted_content: Dict[str, Any], title: str, user_prompt: str = None, ai_service=None) -> str:
"""Create a detailed prompt for AI image generation based on the content.""" """Create a detailed prompt for AI image generation based on the content."""
try: try:
# Start with base prompt # Start with base prompt
@ -174,12 +174,12 @@ Return only the compressed prompt, no explanations.
# Use AI to compress the prompt - call the AI service correctly # Use AI to compress the prompt - call the AI service correctly
# The ai_service has an aiObjects attribute that contains the actual AI interface # The ai_service has an aiObjects attribute that contains the actual AI interface
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
request = AiCallRequest( request = AiCallRequest(
prompt=compression_prompt, prompt=compression_prompt,
options=AiCallOptions( options=AiCallOptions(
operationType=OperationType.GENERAL, operationType=OperationTypeEnum.GENERAL,
maxTokens=None, # Let the model use its full context length maxTokens=None, # Let the model use its full context length
temperature=0.3 # Lower temperature for more consistent compression temperature=0.3 # Lower temperature for more consistent compression
) )

View file

@ -157,10 +157,10 @@ class RendererPdf(BaseRenderer):
return default_styles return default_styles
try: try:
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
request_options = AiCallOptions() request_options = AiCallOptions()
request_options.operationType = OperationType.GENERAL request_options.operationType = OperationTypeEnum.GENERAL
request = AiCallRequest(prompt=style_template, context="", options=request_options) request = AiCallRequest(prompt=style_template, context="", options=request_options)

View file

@ -357,10 +357,10 @@ JSON ONLY. NO OTHER TEXT."""
return default_styles return default_styles
try: try:
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
request_options = AiCallOptions() request_options = AiCallOptions()
request_options.operationType = OperationType.GENERAL request_options.operationType = OperationTypeEnum.GENERAL
request = AiCallRequest(prompt=style_template, context="", options=request_options) request = AiCallRequest(prompt=style_template, context="", options=request_options)

View file

@ -274,10 +274,10 @@ class RendererXlsx(BaseRenderer):
return default_styles return default_styles
try: try:
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
request_options = AiCallOptions() request_options = AiCallOptions()
request_options.operationType = OperationType.GENERAL request_options.operationType = OperationTypeEnum.GENERAL
request = AiCallRequest(prompt=style_template, context="", options=request_options) request = AiCallRequest(prompt=style_template, context="", options=request_options)
response = await ai_service.aiObjects.call(request) response = await ai_service.aiObjects.call(request)

View file

@ -6,7 +6,7 @@ This module builds prompts for AI services to extract and generate documents.
import json import json
import logging import logging
from typing import Dict, Any, Optional, List, TYPE_CHECKING from typing import Dict, Any, Optional, List, TYPE_CHECKING
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
# Type hint for renderer parameter # Type hint for renderer parameter
if TYPE_CHECKING: if TYPE_CHECKING:
@ -380,10 +380,8 @@ Extract the main intent and requirements for document processing. Focus on:
Respond with a clear, concise statement of the extraction intent. Respond with a clear, concise statement of the extraction intent.
""" """
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType
request_options = AiCallOptions() request_options = AiCallOptions()
request_options.operationType = OperationType.GENERAL request_options.operationType = OperationTypeEnum.GENERAL
request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options) request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options)
response = await aiService.aiObjects.call(request) response = await aiService.aiObjects.call(request)

View file

@ -62,7 +62,7 @@ Please provide a comprehensive summary of this conversation."""
documents=None, documents=None,
options={ options={
"process_type": "text", "process_type": "text",
"operation_type": "generate_content", "operation_type": "generate",
"priority": "speed", "priority": "speed",
"compress_prompt": True, "compress_prompt": True,
"compress_documents": False, "compress_documents": False,

View file

@ -10,7 +10,7 @@ from datetime import datetime, UTC
from modules.workflows.methods.methodBase import MethodBase, action from modules.workflows.methods.methodBase import MethodBase, action
from modules.datamodels.datamodelChat import ActionResult from modules.datamodels.datamodelChat import ActionResult
from modules.datamodels.datamodelAi import AiCallOptions, OperationType, Priority from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum, ModelCapabilitiesEnum
from modules.datamodels.datamodelChat import ChatDocument from modules.datamodels.datamodelChat import ChatDocument
from modules.datamodels.datamodelWeb import WebResearchRequest, WebResearchOptions from modules.datamodels.datamodelWeb import WebResearchRequest, WebResearchOptions
@ -42,11 +42,11 @@ class MethodAi(MethodBase):
- resultType (str, optional): Output file extension - only one extension allowed (e.g. txt, json, md, csv, xml, html, pdf, docx, xlsx, png, ...). Default: txt. - resultType (str, optional): Output file extension - only one extension allowed (e.g. txt, json, md, csv, xml, html, pdf, docx, xlsx, png, ...). Default: txt.
- processingMode (str, optional): basic | advanced | detailed. Default: basic. - processingMode (str, optional): basic | advanced | detailed. Default: basic.
- includeMetadata (bool, optional): Include metadata when available. Default: True. - includeMetadata (bool, optional): Include metadata when available. Default: True.
- operationType (str, optional): general | generate_plan | analyse_content | generate_content | web_research | image_analysis | image_generation. Default: general. - operationType (str, optional): general | plan | analyse | generate | webResearch | imageAnalyse | imageGenerate. Default: general.
- priority (str, optional): speed | quality | cost | balanced. Default: balanced. - priority (str, optional): speed | quality | cost | balanced. Default: balanced.
- maxCost (float, optional): Cost limit. - maxCost (float, optional): Cost limit.
- maxProcessingTime (int, optional): Time limit in seconds. - maxProcessingTime (int, optional): Time limit in seconds.
- requiredTags (list, optional): Capability tags (e.g., text, chat, reasoning, analysis, image, vision, web, search). - operationTypes (list, optional): Capability tags (e.g., text, chat, reasoning, analysis, image, vision, web, search).
""" """
try: try:
# Init progress logger # Init progress logger
@ -75,13 +75,55 @@ class MethodAi(MethodBase):
if isinstance(documentList, str): if isinstance(documentList, str):
documentList = [documentList] documentList = [documentList]
resultType = parameters.get("resultType", "txt") resultType = parameters.get("resultType", "txt")
processingMode = parameters.get("processingMode", "basic") processingModeStr = parameters.get("processingMode", "basic")
includeMetadata = parameters.get("includeMetadata", True) includeMetadata = parameters.get("includeMetadata", True)
operationType = parameters.get("operationType", "general") operationTypeStr = parameters.get("operationType", "general")
priority = parameters.get("priority", "balanced") priorityStr = parameters.get("priority", "balanced")
maxCost = parameters.get("maxCost") maxCost = parameters.get("maxCost")
maxProcessingTime = parameters.get("maxProcessingTime") maxProcessingTime = parameters.get("maxProcessingTime")
requiredTags = parameters.get("requiredTags") operationTypes = parameters.get("operationTypes")
requiredTags = parameters.get("requiredTags", [])
# Map string parameters to enums
operationTypeMapping = {
"general": OperationTypeEnum.GENERAL,
"plan": OperationTypeEnum.PLAN,
"analyse": OperationTypeEnum.ANALYSE,
"generate": OperationTypeEnum.GENERATE,
"webResearch": OperationTypeEnum.WEB_RESEARCH,
"imageAnalyse": OperationTypeEnum.IMAGE_ANALYSE,
"imageGenerate": OperationTypeEnum.IMAGE_GENERATE
}
operationType = operationTypeMapping.get(operationTypeStr, OperationTypeEnum.GENERAL)
priorityMapping = {
"speed": PriorityEnum.SPEED,
"quality": PriorityEnum.QUALITY,
"cost": PriorityEnum.COST,
"balanced": PriorityEnum.BALANCED
}
priority = priorityMapping.get(priorityStr, PriorityEnum.BALANCED)
processingModeMapping = {
"basic": ProcessingModeEnum.BASIC,
"advanced": ProcessingModeEnum.ADVANCED,
"detailed": ProcessingModeEnum.DETAILED
}
processingMode = processingModeMapping.get(processingModeStr, ProcessingModeEnum.BASIC)
# Map requiredTags from strings to ModelCapabilitiesEnum
if requiredTags and isinstance(requiredTags, list):
tagMapping = {
"text": ModelCapabilitiesEnum.TEXT_GENERATION,
"chat": ModelCapabilitiesEnum.CHAT,
"reasoning": ModelCapabilitiesEnum.REASONING,
"analysis": ModelCapabilitiesEnum.ANALYSIS,
"image": ModelCapabilitiesEnum.VISION,
"vision": ModelCapabilitiesEnum.VISION,
"web": ModelCapabilitiesEnum.WEB_SEARCH,
"search": ModelCapabilitiesEnum.WEB_SEARCH
}
requiredTags = [tagMapping.get(tag, tag) for tag in requiredTags if isinstance(tag, str)]
if not aiPrompt: if not aiPrompt:
logger.error(f"aiPrompt is missing or empty. Parameters: {parameters}") logger.error(f"aiPrompt is missing or empty. Parameters: {parameters}")
@ -113,14 +155,14 @@ class MethodAi(MethodBase):
options = AiCallOptions( options = AiCallOptions(
operationType=operationType, operationType=operationType,
priority=priority, priority=priority,
compressPrompt=processingMode != "detailed", compressPrompt=processingMode != ProcessingModeEnum.DETAILED,
compressContext=True, compressContext=True,
processDocumentsIndividually=True, processDocumentsIndividually=True,
processingMode=processingMode, processingMode=processingMode,
resultFormat=output_format, resultFormat=output_format,
maxCost=maxCost, maxCost=maxCost,
maxProcessingTime=maxProcessingTime, maxProcessingTime=maxProcessingTime,
requiredTags=requiredTags capabilities=requiredTags if requiredTags else None
) )
# Update progress - calling AI # Update progress - calling AI

View file

@ -116,9 +116,9 @@ DELIVERED CONTENT TO CHECK:
""" """
# Call AI service for validation # Call AI service for validation
from modules.datamodels.datamodelAi import AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
request_options = AiCallOptions() request_options = AiCallOptions()
request_options.operationType = OperationType.GENERAL request_options.operationType = OperationTypeEnum.GENERAL
response = await self.services.ai.callAiPlanning( response = await self.services.ai.callAiPlanning(
prompt=validationPrompt, prompt=validationPrompt,

View file

@ -59,9 +59,9 @@ CRITICAL: Respond with ONLY the JSON object below. Do not include any explanator
""" """
# Call AI service for analysis # Call AI service for analysis
from modules.datamodels.datamodelAi import AiCallOptions, OperationType from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
request_options = AiCallOptions() request_options = AiCallOptions()
request_options.operationType = OperationType.GENERAL request_options.operationType = OperationTypeEnum.GENERAL
response = await self.services.ai.callAiPlanning( response = await self.services.ai.callAiPlanning(
prompt=analysisPrompt, prompt=analysisPrompt,

View file

@ -15,7 +15,7 @@ class ProgressTracker:
self.partialAchievements = [] self.partialAchievements = []
self.failedAttempts = [] self.failedAttempts = []
self.learningInsights = [] self.learningInsights = []
self.currentPhase = "planning" self.currentPhase = "plan"
def updateOperation(self, result: Any, validation: Dict[str, Any], intent: Dict[str, Any]): def updateOperation(self, result: Any, validation: Dict[str, Any], intent: Dict[str, Any]):
"""Updates progress tracking based on action result""" """Updates progress tracking based on action result"""
@ -154,4 +154,4 @@ class ProgressTracker:
self.partialAchievements = [] self.partialAchievements = []
self.failedAttempts = [] self.failedAttempts = []
self.learningInsights = [] self.learningInsights = []
self.currentPhase = "planning" self.currentPhase = "plan"

View file

@ -5,7 +5,7 @@ import json
import logging import logging
from typing import Dict, Any from typing import Dict, Any
from modules.datamodels.datamodelChat import TaskStep, TaskContext, TaskPlan from modules.datamodels.datamodelChat import TaskStep, TaskContext, TaskPlan
from modules.datamodels.datamodelAi import AiCallOptions, OperationType, ProcessingMode, Priority from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, ProcessingModeEnum, PriorityEnum
from modules.workflows.processing.shared.promptGenerationTaskplan import ( from modules.workflows.processing.shared.promptGenerationTaskplan import (
generateTaskPlanningPrompt generateTaskPlanningPrompt
) )
@ -59,7 +59,7 @@ class TaskPlanner:
# Create proper context object for task planning using cleaned intent # Create proper context object for task planning using cleaned intent
# For task planning, we need to create a minimal TaskStep since TaskContext requires it # For task planning, we need to create a minimal TaskStep since TaskContext requires it
planningTaskStep = TaskStep( planningTaskStep = TaskStep(
id="planning", id="plan",
objective=cleanedObjective, objective=cleanedObjective,
dependencies=[], dependencies=[],
success_criteria=[], success_criteria=[],
@ -96,11 +96,11 @@ class TaskPlanner:
# Centralized AI call: Task planning (quality, detailed) with placeholders # Centralized AI call: Task planning (quality, detailed) with placeholders
options = AiCallOptions( options = AiCallOptions(
operationType=OperationType.GENERATE_PLAN, operationType=OperationTypeEnum.PLAN,
priority=Priority.QUALITY, priority=PriorityEnum.QUALITY,
compressPrompt=False, compressPrompt=False,
compressContext=False, compressContext=False,
processingMode=ProcessingMode.DETAILED, processingMode=ProcessingModeEnum.DETAILED,
maxCost=0.10, maxCost=0.10,
maxProcessingTime=30 maxProcessingTime=30
) )

View file

@ -10,7 +10,7 @@ from modules.datamodels.datamodelChat import (
ActionResult, ReviewResult, ReviewContext ActionResult, ReviewResult, ReviewContext
) )
from modules.datamodels.datamodelChat import ChatWorkflow from modules.datamodels.datamodelChat import ChatWorkflow
from modules.datamodels.datamodelAi import AiCallOptions, OperationType, ProcessingMode, Priority from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, ProcessingModeEnum, PriorityEnum
from modules.workflows.processing.modes.modeBase import BaseMode from modules.workflows.processing.modes.modeBase import BaseMode
from modules.workflows.processing.shared.executionState import TaskExecutionState from modules.workflows.processing.shared.executionState import TaskExecutionState
from modules.workflows.processing.shared.promptGenerationActionsActionplan import ( from modules.workflows.processing.shared.promptGenerationActionsActionplan import (
@ -125,11 +125,11 @@ class ActionplanMode(BaseMode):
# Centralized AI call: Action planning (quality, detailed) with placeholders # Centralized AI call: Action planning (quality, detailed) with placeholders
options = AiCallOptions( options = AiCallOptions(
operationType=OperationType.GENERATE_PLAN, operationType=OperationTypeEnum.PLAN,
priority=Priority.QUALITY, priority=PriorityEnum.QUALITY,
compressPrompt=False, compressPrompt=False,
compressContext=False, compressContext=False,
processingMode=ProcessingMode.DETAILED, processingMode=ProcessingModeEnum.DETAILED,
maxCost=0.10, maxCost=0.10,
maxProcessingTime=30 maxProcessingTime=30
) )
@ -457,11 +457,11 @@ class ActionplanMode(BaseMode):
# Centralized AI call: Result validation (balanced analysis) with placeholders # Centralized AI call: Result validation (balanced analysis) with placeholders
options = AiCallOptions( options = AiCallOptions(
operationType=OperationType.ANALYSE_CONTENT, operationType=OperationTypeEnum.ANALYSE,
priority=Priority.BALANCED, priority=PriorityEnum.BALANCED,
compressPrompt=True, compressPrompt=True,
compressContext=False, compressContext=False,
processingMode=ProcessingMode.ADVANCED, processingMode=ProcessingModeEnum.ADVANCED,
maxCost=0.05, maxCost=0.05,
maxProcessingTime=30 maxProcessingTime=30
) )

View file

@ -12,7 +12,7 @@ from modules.datamodels.datamodelChat import (
ActionResult ActionResult
) )
from modules.datamodels.datamodelChat import ChatWorkflow from modules.datamodels.datamodelChat import ChatWorkflow
from modules.datamodels.datamodelAi import AiCallOptions, OperationType, ProcessingMode, Priority from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, ProcessingModeEnum, PriorityEnum
from modules.workflows.processing.modes.modeBase import BaseMode from modules.workflows.processing.modes.modeBase import BaseMode
from modules.workflows.processing.shared.executionState import TaskExecutionState, shouldContinue from modules.workflows.processing.shared.executionState import TaskExecutionState, shouldContinue
from modules.workflows.processing.shared.promptGenerationActionsReact import ( from modules.workflows.processing.shared.promptGenerationActionsReact import (
@ -187,11 +187,11 @@ class ReactMode(BaseMode):
# Centralized AI call for plan selection (use plan generation quality) # Centralized AI call for plan selection (use plan generation quality)
options = AiCallOptions( options = AiCallOptions(
operationType=OperationType.GENERATE_PLAN, operationType=OperationTypeEnum.PLAN,
priority=Priority.QUALITY, priority=PriorityEnum.QUALITY,
compressPrompt=False, compressPrompt=False,
compressContext=False, compressContext=False,
processingMode=ProcessingMode.DETAILED, processingMode=ProcessingModeEnum.DETAILED,
maxCost=0.10, maxCost=0.10,
maxProcessingTime=30 maxProcessingTime=30
) )
@ -296,11 +296,11 @@ class ReactMode(BaseMode):
# Centralized AI call for parameter suggestion (balanced analysis) # Centralized AI call for parameter suggestion (balanced analysis)
options = AiCallOptions( options = AiCallOptions(
operationType=OperationType.ANALYSE_CONTENT, operationType=OperationTypeEnum.ANALYSE,
priority=Priority.BALANCED, priority=PriorityEnum.BALANCED,
compressPrompt=True, compressPrompt=True,
compressContext=False, compressContext=False,
processingMode=ProcessingMode.ADVANCED, processingMode=ProcessingModeEnum.ADVANCED,
maxCost=0.05, maxCost=0.05,
maxProcessingTime=30, maxProcessingTime=30,
temperature=0.3, # Slightly higher temperature for better instruction following temperature=0.3, # Slightly higher temperature for better instruction following
@ -611,11 +611,11 @@ class ReactMode(BaseMode):
# Centralized AI call for refinement decision (balanced analysis) # Centralized AI call for refinement decision (balanced analysis)
options = AiCallOptions( options = AiCallOptions(
operationType=OperationType.ANALYSE_CONTENT, operationType=OperationTypeEnum.ANALYSE,
priority=Priority.BALANCED, priority=PriorityEnum.BALANCED,
compressPrompt=True, compressPrompt=True,
compressContext=False, compressContext=False,
processingMode=ProcessingMode.ADVANCED, processingMode=ProcessingModeEnum.ADVANCED,
maxCost=0.05, maxCost=0.05,
maxProcessingTime=30 maxProcessingTime=30
) )
@ -718,8 +718,8 @@ Return only the user-friendly message, no technical details."""
prompt=prompt, prompt=prompt,
placeholders=None, placeholders=None,
options=AiCallOptions( options=AiCallOptions(
operationType=OperationType.GENERATE_CONTENT, operationType=OperationTypeEnum.GENERATE,
priority=Priority.SPEED, priority=PriorityEnum.SPEED,
compressPrompt=True, compressPrompt=True,
maxCost=0.01, maxCost=0.01,
maxProcessingTime=5 maxProcessingTime=5
@ -759,8 +759,8 @@ Return only the user-friendly message, no technical details."""
prompt=prompt, prompt=prompt,
placeholders=None, placeholders=None,
options=AiCallOptions( options=AiCallOptions(
operationType=OperationType.GENERATE_CONTENT, operationType=OperationTypeEnum.GENERATE,
priority=Priority.SPEED, priority=PriorityEnum.SPEED,
compressPrompt=True, compressPrompt=True,
maxCost=0.01, maxCost=0.01,
maxProcessingTime=5 maxProcessingTime=5