implemented dynmaic ai integration and selection chain
This commit is contained in:
parent
3adaaad8eb
commit
109e77fd60
34 changed files with 1331 additions and 2316 deletions
|
|
@ -71,10 +71,10 @@ class BaseConnectorAi(ABC):
|
|||
models = self.getCachedModels()
|
||||
return [model for model in models if capability in model.capabilities]
|
||||
|
||||
def getModelsByTag(self, tag: str) -> List[AiModel]:
|
||||
"""Get models that have a specific tag."""
|
||||
def getModelsByPriority(self, priority: str) -> List[AiModel]:
|
||||
"""Get models that have a specific priority."""
|
||||
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]:
|
||||
"""Get only available models."""
|
||||
|
|
|
|||
|
|
@ -19,40 +19,40 @@ class ModelRegistry:
|
|||
def __init__(self):
|
||||
self._models: Dict[str, AiModel] = {}
|
||||
self._connectors: Dict[str, BaseConnectorAi] = {}
|
||||
self._last_refresh: Optional[float] = None
|
||||
self._refresh_interval: float = 300.0 # 5 minutes
|
||||
self._lastRefresh: Optional[float] = None
|
||||
self._refreshInterval: float = 300.0 # 5 minutes
|
||||
|
||||
def registerConnector(self, connector: BaseConnectorAi):
|
||||
"""Register a connector and collect its models."""
|
||||
connector_type = connector.getConnectorType()
|
||||
self._connectors[connector_type] = connector
|
||||
connectorType = connector.getConnectorType()
|
||||
self._connectors[connectorType] = connector
|
||||
|
||||
# Collect models from this connector
|
||||
try:
|
||||
models = connector.getCachedModels()
|
||||
for model in models:
|
||||
self._models[model.name] = model
|
||||
logger.debug(f"Registered model: {model.name} from {connector_type}")
|
||||
logger.debug(f"Registered model: {model.name} from {connectorType}")
|
||||
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]:
|
||||
"""Auto-discover connectors by scanning aicorePlugin*.py files."""
|
||||
connectors = []
|
||||
connector_dir = os.path.dirname(__file__)
|
||||
connectorDir = os.path.dirname(__file__)
|
||||
|
||||
# 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'):
|
||||
module_name = filename[:-3] # Remove .py extension
|
||||
moduleName = filename[:-3] # Remove .py extension
|
||||
|
||||
try:
|
||||
# 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)
|
||||
for attr_name in dir(module):
|
||||
attr = getattr(module, attr_name)
|
||||
for attrName in dir(module):
|
||||
attr = getattr(module, attrName)
|
||||
if (isinstance(attr, type) and
|
||||
issubclass(attr, BaseConnectorAi) and
|
||||
attr != BaseConnectorAi):
|
||||
|
|
@ -71,12 +71,12 @@ class ModelRegistry:
|
|||
"""Refresh models from all registered connectors."""
|
||||
import time
|
||||
|
||||
current_time = time.time()
|
||||
currentTime = time.time()
|
||||
|
||||
# Check if refresh is needed
|
||||
if (not force and
|
||||
self._last_refresh is not None and
|
||||
current_time - self._last_refresh < self._refresh_interval):
|
||||
self._lastRefresh is not None and
|
||||
currentTime - self._lastRefresh < self._refreshInterval):
|
||||
return
|
||||
|
||||
logger.info("Refreshing model registry...")
|
||||
|
|
@ -94,7 +94,7 @@ class ModelRegistry:
|
|||
except Exception as 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")
|
||||
|
||||
def getModel(self, name: str) -> Optional[AiModel]:
|
||||
|
|
@ -107,29 +107,29 @@ class ModelRegistry:
|
|||
self.refreshModels()
|
||||
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."""
|
||||
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]:
|
||||
"""Get models that support a specific capability."""
|
||||
self.refreshModels()
|
||||
return [model for model in self._models.values() if capability in model.capabilities]
|
||||
|
||||
def getModelsByTag(self, tag: str) -> List[AiModel]:
|
||||
"""Get models that have a specific tag."""
|
||||
def getModelsByPriority(self, priority: str) -> List[AiModel]:
|
||||
"""Get models that have a specific priority."""
|
||||
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]:
|
||||
"""Get only available models."""
|
||||
self.refreshModels()
|
||||
return [model for model in self._models.values() if model.isAvailable]
|
||||
|
||||
def getConnectorForModel(self, model_name: str) -> Optional[BaseConnectorAi]:
|
||||
def getConnectorForModel(self, modelName: str) -> Optional[BaseConnectorAi]:
|
||||
"""Get the connector instance for a specific model."""
|
||||
model = self.getModel(model_name)
|
||||
model = self.getModel(modelName)
|
||||
if model:
|
||||
return self._connectors.get(model.connectorType)
|
||||
return None
|
||||
|
|
@ -139,34 +139,34 @@ class ModelRegistry:
|
|||
self.refreshModels()
|
||||
|
||||
stats = {
|
||||
"total_models": len(self._models),
|
||||
"available_models": len([m for m in self._models.values() if m.isAvailable]),
|
||||
"totalModels": len(self._models),
|
||||
"availableModels": len([m for m in self._models.values() if m.isAvailable]),
|
||||
"connectors": len(self._connectors),
|
||||
"by_connector": {},
|
||||
"by_capability": {},
|
||||
"by_tag": {}
|
||||
"byConnector": {},
|
||||
"byCapability": {},
|
||||
"byPriority": {}
|
||||
}
|
||||
|
||||
# Count by connector
|
||||
for model in self._models.values():
|
||||
connector = model.connectorType
|
||||
if connector not in stats["by_connector"]:
|
||||
stats["by_connector"][connector] = 0
|
||||
stats["by_connector"][connector] += 1
|
||||
if connector not in stats["byConnector"]:
|
||||
stats["byConnector"][connector] = 0
|
||||
stats["byConnector"][connector] += 1
|
||||
|
||||
# Count by capability
|
||||
for model in self._models.values():
|
||||
for capability in model.capabilities:
|
||||
if capability not in stats["by_capability"]:
|
||||
stats["by_capability"][capability] = 0
|
||||
stats["by_capability"][capability] += 1
|
||||
if capability not in stats["byCapability"]:
|
||||
stats["byCapability"][capability] = 0
|
||||
stats["byCapability"][capability] += 1
|
||||
|
||||
# Count by tag
|
||||
# Count by priority
|
||||
for model in self._models.values():
|
||||
for tag in model.tags:
|
||||
if tag not in stats["by_tag"]:
|
||||
stats["by_tag"][tag] = 0
|
||||
stats["by_tag"][tag] += 1
|
||||
priority = model.priority
|
||||
if priority not in stats["byPriority"]:
|
||||
stats["byPriority"][priority] = 0
|
||||
stats["byPriority"][priority] += 1
|
||||
|
||||
return stats
|
||||
|
||||
|
|
|
|||
|
|
@ -3,24 +3,8 @@ Configuration for dynamic model selection rules.
|
|||
This makes model selection configurable rather than hardcoded.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from modules.datamodels.datamodelAi import OperationType, Priority, ProcessingMode, ModelTags
|
||||
|
||||
|
||||
@dataclass
|
||||
class SelectionRule:
|
||||
"""A rule for model selection."""
|
||||
name: str
|
||||
condition: str # Description of when this rule applies
|
||||
weight: float # Weight for scoring (higher = more important)
|
||||
operation_types: List[str] # Operation types this rule applies to
|
||||
required_tags: List[str] # Required tags for this rule
|
||||
preferred_tags: List[str] # Preferred tags for this rule
|
||||
avoid_tags: List[str] # Tags to avoid for this rule
|
||||
min_quality_rating: Optional[int] = None # Minimum quality rating
|
||||
max_cost: Optional[float] = None # Maximum cost threshold
|
||||
min_context_length: Optional[int] = None # Minimum context length required
|
||||
from typing import Dict, List, Any
|
||||
from modules.datamodels.datamodelAi import OperationTypeEnum, ModelCapabilitiesEnum, PriorityEnum, SelectionRule
|
||||
|
||||
|
||||
class ModelSelectionConfig:
|
||||
|
|
@ -28,148 +12,142 @@ class ModelSelectionConfig:
|
|||
|
||||
def __init__(self):
|
||||
self.rules = self._loadDefaultRules()
|
||||
self.fallback_models = self._loadFallbackModels()
|
||||
self.fallbackModels = self._loadFallbackModels()
|
||||
|
||||
def _loadDefaultRules(self) -> List[SelectionRule]:
|
||||
"""Load default selection rules."""
|
||||
return [
|
||||
# High quality for planning and analysis
|
||||
SelectionRule(
|
||||
name="high_quality_analysis",
|
||||
name="highQualityAnalysis",
|
||||
condition="Planning or analysis operations requiring high quality",
|
||||
weight=10.0,
|
||||
operation_types=[OperationType.GENERATE_PLAN, OperationType.ANALYSE_CONTENT],
|
||||
required_tags=[ModelTags.TEXT, ModelTags.REASONING, ModelTags.ANALYSIS],
|
||||
preferred_tags=[ModelTags.HIGH_QUALITY],
|
||||
avoid_tags=[ModelTags.FAST],
|
||||
min_quality_rating=8
|
||||
operationTypes=[OperationTypeEnum.PLAN, OperationTypeEnum.ANALYSE],
|
||||
priority=PriorityEnum.QUALITY,
|
||||
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.ANALYSIS],
|
||||
minQualityRating=8
|
||||
),
|
||||
|
||||
# Fast processing for basic operations
|
||||
SelectionRule(
|
||||
name="fast_basic_processing",
|
||||
name="fastBasicProcessing",
|
||||
condition="Basic operations requiring speed",
|
||||
weight=8.0,
|
||||
operation_types=[OperationType.GENERAL],
|
||||
required_tags=[ModelTags.TEXT, ModelTags.CHAT],
|
||||
preferred_tags=[ModelTags.FAST],
|
||||
avoid_tags=[],
|
||||
min_quality_rating=5
|
||||
operationTypes=[OperationTypeEnum.GENERAL],
|
||||
priority=PriorityEnum.SPEED,
|
||||
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT],
|
||||
minQualityRating=5
|
||||
),
|
||||
|
||||
# Cost-effective for high-volume operations
|
||||
SelectionRule(
|
||||
name="cost_effective_processing",
|
||||
name="costEffectiveProcessing",
|
||||
condition="High-volume operations where cost matters",
|
||||
weight=7.0,
|
||||
operation_types=[OperationType.GENERAL, OperationType.GENERATE_CONTENT],
|
||||
required_tags=[ModelTags.TEXT],
|
||||
preferred_tags=[ModelTags.COST_EFFECTIVE],
|
||||
avoid_tags=[],
|
||||
max_cost=0.01 # $0.01 per 1k tokens
|
||||
operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.GENERATE],
|
||||
priority=PriorityEnum.COST,
|
||||
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION],
|
||||
maxCost=0.01 # $0.01 per 1k tokens
|
||||
),
|
||||
|
||||
# Image analysis specific
|
||||
SelectionRule(
|
||||
name="image_analysis",
|
||||
name="imageAnalyse",
|
||||
condition="Image analysis operations",
|
||||
weight=10.0,
|
||||
operation_types=[OperationType.IMAGE_ANALYSIS],
|
||||
required_tags=[ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL],
|
||||
preferred_tags=[ModelTags.HIGH_QUALITY],
|
||||
avoid_tags=[],
|
||||
min_quality_rating=8
|
||||
operationTypes=[OperationTypeEnum.IMAGE_ANALYSE],
|
||||
priority=PriorityEnum.QUALITY,
|
||||
capabilities=[ModelCapabilitiesEnum.VISION, ModelCapabilitiesEnum.MULTIMODAL],
|
||||
minQualityRating=8
|
||||
),
|
||||
|
||||
# Web research specific
|
||||
SelectionRule(
|
||||
name="web_research",
|
||||
name="webResearch",
|
||||
condition="Web research operations",
|
||||
weight=9.0,
|
||||
operation_types=[OperationType.WEB_RESEARCH],
|
||||
required_tags=[ModelTags.TEXT, ModelTags.ANALYSIS],
|
||||
preferred_tags=[ModelTags.WEB, ModelTags.SEARCH],
|
||||
avoid_tags=[],
|
||||
min_quality_rating=7
|
||||
operationTypes=[OperationTypeEnum.WEB_RESEARCH],
|
||||
priority=PriorityEnum.BALANCED,
|
||||
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.ANALYSIS, ModelCapabilitiesEnum.WEB_SEARCH],
|
||||
minQualityRating=7
|
||||
),
|
||||
|
||||
# Large context requirements
|
||||
SelectionRule(
|
||||
name="large_context",
|
||||
name="largeContext",
|
||||
condition="Operations requiring large context",
|
||||
weight=8.0,
|
||||
operation_types=[OperationType.GENERAL, OperationType.ANALYSE_CONTENT],
|
||||
required_tags=[ModelTags.TEXT],
|
||||
preferred_tags=[],
|
||||
avoid_tags=[],
|
||||
min_context_length=100000 # 100k tokens
|
||||
operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.ANALYSE],
|
||||
priority=PriorityEnum.BALANCED,
|
||||
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION],
|
||||
minContextLength=100000 # 100k tokens
|
||||
)
|
||||
]
|
||||
|
||||
def _loadFallbackModels(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Load fallback model selection criteria."""
|
||||
return {
|
||||
OperationType.GENERAL: {
|
||||
"priority_order": ["speed", "quality", "cost"],
|
||||
"required_tags": [ModelTags.TEXT, ModelTags.CHAT],
|
||||
"min_quality_rating": 5,
|
||||
"max_cost_per_1k": 0.01
|
||||
OperationTypeEnum.GENERAL: {
|
||||
"priorityOrder": ["speed", "quality", "cost"],
|
||||
"operationTypes": [ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT],
|
||||
"minQualityRating": 5,
|
||||
"maxCostPer1k": 0.01
|
||||
},
|
||||
OperationType.IMAGE_ANALYSIS: {
|
||||
"priority_order": ["quality", "speed"],
|
||||
"required_tags": [ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL],
|
||||
"min_quality_rating": 8,
|
||||
"max_cost_per_1k": 0.1
|
||||
OperationTypeEnum.IMAGE_ANALYSE: {
|
||||
"priorityOrder": ["quality", "speed"],
|
||||
"operationTypes": [ModelCapabilitiesEnum.VISION, ModelCapabilitiesEnum.MULTIMODAL],
|
||||
"minQualityRating": 8,
|
||||
"maxCostPer1k": 0.1
|
||||
},
|
||||
OperationType.IMAGE_GENERATION: {
|
||||
"priority_order": ["quality", "speed"],
|
||||
"required_tags": [ModelTags.IMAGE_GENERATION, ModelTags.ART, ModelTags.VISUAL],
|
||||
"min_quality_rating": 8,
|
||||
"max_cost_per_1k": 0.1
|
||||
OperationTypeEnum.IMAGE_GENERATE: {
|
||||
"priorityOrder": ["quality", "speed"],
|
||||
"operationTypes": [ModelCapabilitiesEnum.IMAGE_GENERATE, ModelCapabilitiesEnum.ART, ModelCapabilitiesEnum.VISUAL_CREATION],
|
||||
"minQualityRating": 8,
|
||||
"maxCostPer1k": 0.1
|
||||
},
|
||||
OperationType.WEB_RESEARCH: {
|
||||
"priority_order": ["quality", "speed", "cost"],
|
||||
"required_tags": [ModelTags.TEXT, ModelTags.ANALYSIS],
|
||||
"preferred_tags": [ModelTags.WEB, ModelTags.SEARCH],
|
||||
"min_quality_rating": 7,
|
||||
"max_cost_per_1k": 0.02
|
||||
OperationTypeEnum.WEB_RESEARCH: {
|
||||
"priorityOrder": ["quality", "speed", "cost"],
|
||||
"operationTypes": [ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.ANALYSIS],
|
||||
"preferredTags": [ModelCapabilitiesEnum.WEB_SEARCH],
|
||||
"minQualityRating": 7,
|
||||
"maxCostPer1k": 0.02
|
||||
},
|
||||
OperationType.GENERATE_PLAN: {
|
||||
"priority_order": ["quality", "speed"],
|
||||
"required_tags": [ModelTags.TEXT, ModelTags.REASONING, ModelTags.ANALYSIS],
|
||||
"preferred_tags": [ModelTags.HIGH_QUALITY],
|
||||
"min_quality_rating": 8,
|
||||
"max_cost_per_1k": 0.1
|
||||
OperationTypeEnum.PLAN: {
|
||||
"priorityOrder": ["quality", "speed"],
|
||||
"operationTypes": [ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.ANALYSIS],
|
||||
"preferredTags": [PriorityEnum.QUALITY],
|
||||
"minQualityRating": 8,
|
||||
"maxCostPer1k": 0.1
|
||||
},
|
||||
OperationType.ANALYSE_CONTENT: {
|
||||
"priority_order": ["quality", "speed"],
|
||||
"required_tags": [ModelTags.TEXT, ModelTags.ANALYSIS, ModelTags.REASONING],
|
||||
"preferred_tags": [ModelTags.HIGH_QUALITY],
|
||||
"min_quality_rating": 8,
|
||||
"max_cost_per_1k": 0.1
|
||||
OperationTypeEnum.ANALYSE: {
|
||||
"priorityOrder": ["quality", "speed"],
|
||||
"operationTypes": [ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.ANALYSIS, ModelCapabilitiesEnum.REASONING],
|
||||
"preferredTags": [PriorityEnum.QUALITY],
|
||||
"minQualityRating": 8,
|
||||
"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."""
|
||||
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."""
|
||||
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):
|
||||
"""Add a new selection rule."""
|
||||
self.rules.append(rule)
|
||||
|
||||
def removeRule(self, rule_name: str):
|
||||
def removeRule(self, ruleName: str):
|
||||
"""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."""
|
||||
for rule in self.rules:
|
||||
if rule.name == rule_name:
|
||||
if rule.name == ruleName:
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(rule, key):
|
||||
setattr(rule, key, value)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Dynamic model selector using configurable rules and scoring.
|
|||
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
from modules.datamodels.datamodelAi import AiModel, AiCallOptions, OperationType, Priority, ProcessingMode, ModelTags
|
||||
from modules.datamodels.datamodelAi import AiModel, AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum, ModelCapabilitiesEnum
|
||||
from modules.aicore.aicoreModelSelectionConfig import model_selection_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -20,7 +20,7 @@ class ModelSelector:
|
|||
prompt: str,
|
||||
context: str,
|
||||
options: AiCallOptions,
|
||||
available_models: List[AiModel]) -> Optional[AiModel]:
|
||||
availableModels: List[AiModel]) -> Optional[AiModel]:
|
||||
"""
|
||||
Select the best model based on configurable rules and scoring.
|
||||
|
||||
|
|
@ -28,171 +28,171 @@ class ModelSelector:
|
|||
prompt: User prompt
|
||||
context: Context data
|
||||
options: AI call options
|
||||
available_models: List of available models to choose from
|
||||
availableModels: List of available models to choose from
|
||||
|
||||
Returns:
|
||||
Selected model or None if no suitable model found
|
||||
"""
|
||||
if not available_models:
|
||||
if not availableModels:
|
||||
logger.warning("No models available for selection")
|
||||
return None
|
||||
|
||||
logger.info(f"Selecting model for operation: {options.operationType}, priority: {options.priority}")
|
||||
|
||||
# Calculate input size
|
||||
input_size = len(prompt.encode("utf-8")) + len(context.encode("utf-8"))
|
||||
inputSize = len(prompt.encode("utf-8")) + len(context.encode("utf-8"))
|
||||
|
||||
# Get applicable rules
|
||||
rules = self.config.getRulesForOperation(options.operationType)
|
||||
logger.debug(f"Found {len(rules)} applicable rules for {options.operationType}")
|
||||
|
||||
# Score each model
|
||||
scored_models = []
|
||||
for model in available_models:
|
||||
scoredModels = []
|
||||
for model in availableModels:
|
||||
if not model.isAvailable:
|
||||
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
|
||||
scored_models.append((model, score))
|
||||
scoredModels.append((model, score))
|
||||
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")
|
||||
# Try fallback criteria
|
||||
fallback_criteria = self.getFallbackCriteria(options.operationType)
|
||||
return self._selectWithFallbackCriteria(available_models, fallback_criteria, input_size, options)
|
||||
fallbackCriteria = self.getFallbackCriteria(options.operationType)
|
||||
return self._selectWithFallbackCriteria(availableModels, fallbackCriteria, inputSize, options)
|
||||
|
||||
# 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]
|
||||
selected_score = scored_models[0][1]
|
||||
selectedModel = scoredModels[0][0]
|
||||
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
|
||||
self._logSelectionDetails(selected_model, input_size, options)
|
||||
self._logSelectionDetails(selectedModel, inputSize, options)
|
||||
|
||||
return selected_model
|
||||
return selectedModel
|
||||
|
||||
def _calculateModelScore(self,
|
||||
model: AiModel,
|
||||
input_size: int,
|
||||
inputSize: int,
|
||||
options: AiCallOptions,
|
||||
rules: List) -> float:
|
||||
"""Calculate score for a model based on rules and criteria."""
|
||||
score = 0.0
|
||||
|
||||
# Check basic requirements
|
||||
if not self._meetsBasicRequirements(model, input_size, options):
|
||||
if not self._meetsBasicRequirements(model, inputSize, options):
|
||||
return 0.0
|
||||
|
||||
# Apply rules
|
||||
for rule in rules:
|
||||
rule_score = self._applyRule(model, input_size, options, rule)
|
||||
score += rule_score * rule.weight
|
||||
ruleScore = self._applyRule(model, inputSize, options, rule)
|
||||
score += ruleScore * rule.weight
|
||||
|
||||
# Apply priority-based scoring
|
||||
priority_score = self._applyPriorityScoring(model, options)
|
||||
score += priority_score
|
||||
priorityScore = self._applyPriorityScoring(model, options)
|
||||
score += priorityScore
|
||||
|
||||
# Apply processing mode scoring
|
||||
mode_score = self._applyProcessingModeScoring(model, options)
|
||||
score += mode_score
|
||||
modeScore = self._applyProcessingModeScoring(model, options)
|
||||
score += modeScore
|
||||
|
||||
# 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
|
||||
|
||||
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."""
|
||||
# Context length check
|
||||
if model.contextLength > 0 and input_size > model.contextLength * 0.8:
|
||||
logger.debug(f"Model {model.name} rejected: input too large ({input_size} > {model.contextLength * 0.8})")
|
||||
if model.contextLength > 0 and inputSize > model.contextLength * 0.8:
|
||||
logger.debug(f"Model {model.name} rejected: input too large ({inputSize} > {model.contextLength * 0.8})")
|
||||
return False
|
||||
|
||||
# Required tags check
|
||||
if options.requiredTags:
|
||||
if not all(tag in model.tags for tag in options.requiredTags):
|
||||
logger.debug(f"Model {model.name} rejected: missing required tags")
|
||||
# Required operation types check
|
||||
if options.operationTypes:
|
||||
if not all(opType in model.operationTypes for opType in options.operationTypes):
|
||||
logger.debug(f"Model {model.name} rejected: missing required operation types")
|
||||
return False
|
||||
|
||||
# Capabilities check
|
||||
if options.modelCapabilities:
|
||||
if not all(cap in model.capabilities for cap in options.modelCapabilities):
|
||||
if options.capabilities:
|
||||
if not all(cap in model.capabilities for cap in options.capabilities):
|
||||
logger.debug(f"Model {model.name} rejected: missing required capabilities")
|
||||
return False
|
||||
|
||||
# Avoid tags check
|
||||
# Avoid operation types check
|
||||
for rule in self.config.getRulesForOperation(options.operationType):
|
||||
if any(tag in model.tags for tag in rule.avoid_tags):
|
||||
logger.debug(f"Model {model.name} rejected: has avoid tags")
|
||||
if any(opType in model.operationTypes for opType in rule.avoidOperationTypes):
|
||||
logger.debug(f"Model {model.name} rejected: has avoid operation types")
|
||||
return False
|
||||
|
||||
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."""
|
||||
score = 0.0
|
||||
|
||||
# Required tags match
|
||||
if all(tag in model.tags for tag in rule.required_tags):
|
||||
# Required operation types match
|
||||
if all(opType in model.operationTypes for opType in rule.operationTypes):
|
||||
score += 1.0
|
||||
|
||||
# Preferred tags match
|
||||
preferred_matches = sum(1 for tag in rule.preferred_tags if tag in model.tags)
|
||||
if rule.preferred_tags:
|
||||
score += (preferred_matches / len(rule.preferred_tags)) * 0.5
|
||||
# Preferred capabilities match
|
||||
preferredMatches = sum(1 for cap in rule.preferredCapabilities if cap in model.capabilities)
|
||||
if rule.preferredCapabilities:
|
||||
score += (preferredMatches / len(rule.preferredCapabilities)) * 0.5
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
return score
|
||||
|
||||
def _applyPriorityScoring(self, model: AiModel, options: AiCallOptions) -> float:
|
||||
"""Apply priority-based scoring."""
|
||||
if options.priority == Priority.SPEED:
|
||||
if options.priority == PriorityEnum.SPEED:
|
||||
return model.speedRating * 0.1
|
||||
elif options.priority == Priority.QUALITY:
|
||||
elif options.priority == PriorityEnum.QUALITY:
|
||||
return model.qualityRating * 0.1
|
||||
elif options.priority == Priority.COST:
|
||||
elif options.priority == PriorityEnum.COST:
|
||||
# Lower cost = higher score
|
||||
cost_score = max(0, 1.0 - (model.costPer1kTokens * 1000))
|
||||
return cost_score * 0.1
|
||||
costScore = max(0, 1.0 - (model.costPer1kTokensInput * 1000))
|
||||
return costScore * 0.1
|
||||
else: # BALANCED
|
||||
return (model.qualityRating + model.speedRating) * 0.05
|
||||
|
||||
def _applyProcessingModeScoring(self, model: AiModel, options: AiCallOptions) -> float:
|
||||
"""Apply processing mode scoring."""
|
||||
if options.processingMode == ProcessingMode.DETAILED:
|
||||
if ModelTags.HIGH_QUALITY in model.tags:
|
||||
if options.processingMode == ProcessingModeEnum.DETAILED:
|
||||
if model.priority == PriorityEnum.QUALITY:
|
||||
return 0.2
|
||||
elif options.processingMode == ProcessingMode.BASIC:
|
||||
if ModelTags.FAST in model.tags:
|
||||
elif options.processingMode == ProcessingModeEnum.BASIC:
|
||||
if model.priority == PriorityEnum.SPEED:
|
||||
return 0.2
|
||||
|
||||
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."""
|
||||
if options.maxCost is None:
|
||||
return True
|
||||
|
||||
# Estimate cost
|
||||
estimated_tokens = input_size / 4
|
||||
estimated_cost = (estimated_tokens / 1000) * model.costPer1kTokens
|
||||
estimatedTokens = inputSize / 4
|
||||
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."""
|
||||
logger.info(f"Model Selection Details:")
|
||||
logger.info(f" Selected: {model.displayName} ({model.name})")
|
||||
|
|
@ -200,50 +200,50 @@ class ModelSelector:
|
|||
logger.info(f" Operation: {options.operationType}")
|
||||
logger.info(f" Priority: {options.priority}")
|
||||
logger.info(f" Processing Mode: {options.processingMode}")
|
||||
logger.info(f" Input Size: {input_size} bytes")
|
||||
logger.info(f" Input Size: {inputSize} bytes")
|
||||
logger.info(f" Context Length: {model.contextLength}")
|
||||
logger.info(f" Max Tokens: {model.maxTokens}")
|
||||
logger.info(f" Quality Rating: {model.qualityRating}/10")
|
||||
logger.info(f" Speed Rating: {model.speedRating}/10")
|
||||
logger.info(f" Cost: ${model.costPer1kTokens:.4f}/1k tokens")
|
||||
logger.info(f" Cost: ${model.costPer1kTokensInput:.4f}/1k tokens")
|
||||
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."""
|
||||
return self.config.getFallbackCriteria(operation_type)
|
||||
return self.config.getFallbackCriteria(operationType)
|
||||
|
||||
def _selectWithFallbackCriteria(self,
|
||||
available_models: List[AiModel],
|
||||
fallback_criteria: Dict[str, Any],
|
||||
input_size: int,
|
||||
availableModels: List[AiModel],
|
||||
fallbackCriteria: Dict[str, Any],
|
||||
inputSize: int,
|
||||
options: AiCallOptions) -> Optional[AiModel]:
|
||||
"""Select model using fallback criteria when normal selection fails."""
|
||||
logger.info("Using fallback criteria for model selection")
|
||||
|
||||
# Filter models by fallback criteria
|
||||
candidates = []
|
||||
for model in available_models:
|
||||
for model in availableModels:
|
||||
if not model.isAvailable:
|
||||
continue
|
||||
|
||||
# Check required tags
|
||||
if fallback_criteria.get("required_tags"):
|
||||
if not all(tag in model.tags for tag in fallback_criteria["required_tags"]):
|
||||
# Check required operation types
|
||||
if fallbackCriteria.get("operationTypes"):
|
||||
if not all(opType in model.operationTypes for opType in fallbackCriteria["operationTypes"]):
|
||||
continue
|
||||
|
||||
# Check quality rating
|
||||
if fallback_criteria.get("min_quality_rating"):
|
||||
if model.qualityRating < fallback_criteria["min_quality_rating"]:
|
||||
if fallbackCriteria.get("minQualityRating"):
|
||||
if model.qualityRating < fallbackCriteria["minQualityRating"]:
|
||||
continue
|
||||
|
||||
# Check cost
|
||||
if fallback_criteria.get("max_cost_per_1k"):
|
||||
if model.costPer1kTokens > fallback_criteria["max_cost_per_1k"]:
|
||||
if fallbackCriteria.get("maxCostPer1k"):
|
||||
if model.costPer1kTokensInput > fallbackCriteria["maxCostPer1k"]:
|
||||
continue
|
||||
|
||||
# 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
|
||||
|
||||
candidates.append(model)
|
||||
|
|
@ -253,26 +253,133 @@ class ModelSelector:
|
|||
return None
|
||||
|
||||
# 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
|
||||
for i, priority in enumerate(priority_order):
|
||||
weight = len(priority_order) - i # Higher weight for earlier priorities
|
||||
for i, priority in enumerate(priorityOrder):
|
||||
weight = len(priorityOrder) - i # Higher weight for earlier priorities
|
||||
if priority == "quality":
|
||||
score += model.qualityRating * weight
|
||||
elif priority == "speed":
|
||||
score += model.speedRating * weight
|
||||
elif priority == "cost":
|
||||
# Lower cost = higher score
|
||||
score += (1.0 - model.costPer1kTokens * 1000) * weight
|
||||
score += (1.0 - model.costPer1kTokensInput * 1000) * weight
|
||||
return score
|
||||
|
||||
candidates.sort(key=get_priority_score, reverse=True)
|
||||
selected_model = candidates[0]
|
||||
candidates.sort(key=_getPriorityScore, reverse=True)
|
||||
selectedModel = candidates[0]
|
||||
|
||||
logger.info(f"Fallback selection: {selected_model.name} (score: {get_priority_score(selected_model):.2f})")
|
||||
return selected_model
|
||||
logger.info(f"Fallback selection: {selectedModel.name} (score: {_getPriorityScore(selectedModel):.2f})")
|
||||
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
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from typing import Dict, Any, List, Union
|
|||
from fastapi import HTTPException
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -55,17 +55,17 @@ class AiAnthropic(BaseConnectorAi):
|
|||
connectorType="anthropic",
|
||||
maxTokens=200000,
|
||||
contextLength=200000,
|
||||
costPer1kTokens=0.015,
|
||||
costPer1kTokensInput=0.015,
|
||||
costPer1kTokensOutput=0.075,
|
||||
speedRating=7,
|
||||
qualityRating=10,
|
||||
capabilities=["text_generation", "chat", "reasoning", "analysis"],
|
||||
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.ANALYSIS, ModelTags.HIGH_QUALITY],
|
||||
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.ANALYSIS],
|
||||
functionCall=self.callAiBasic,
|
||||
priority="quality",
|
||||
processingMode="detailed",
|
||||
preferredFor=["generate_plan", "analyse_content"],
|
||||
version="claude-3-5-sonnet-20241022"
|
||||
priority=PriorityEnum.QUALITY,
|
||||
processingMode=ProcessingModeEnum.DETAILED,
|
||||
operationTypes=[OperationTypeEnum.PLAN, OperationTypeEnum.ANALYSE],
|
||||
version="claude-3-5-sonnet-20241022",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
|
||||
),
|
||||
AiModel(
|
||||
name="anthropic_callAiImage",
|
||||
|
|
@ -73,17 +73,17 @@ class AiAnthropic(BaseConnectorAi):
|
|||
connectorType="anthropic",
|
||||
maxTokens=200000,
|
||||
contextLength=200000,
|
||||
costPer1kTokens=0.015,
|
||||
costPer1kTokensInput=0.015,
|
||||
costPer1kTokensOutput=0.075,
|
||||
speedRating=7,
|
||||
qualityRating=10,
|
||||
capabilities=["image_analysis", "vision", "multimodal"],
|
||||
tags=[ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL, ModelTags.HIGH_QUALITY],
|
||||
capabilities=[ModelCapabilitiesEnum.IMAGE_ANALYSE, ModelCapabilitiesEnum.VISION, ModelCapabilitiesEnum.MULTIMODAL],
|
||||
functionCall=self.callAiImage,
|
||||
priority="quality",
|
||||
processingMode="detailed",
|
||||
preferredFor=["image_analysis"],
|
||||
version="claude-3-5-sonnet-20241022"
|
||||
priority=PriorityEnum.QUALITY,
|
||||
processingMode=ProcessingModeEnum.DETAILED,
|
||||
operationTypes=[OperationTypeEnum.IMAGE_ANALYSE],
|
||||
version="claude-3-5-sonnet-20241022",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
233
modules/aicore/aicorePluginInternal.py
Normal file
233
modules/aicore/aicorePluginInternal.py
Normal 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
|
||||
|
|
@ -5,7 +5,7 @@ from typing import Dict, Any, List, Union
|
|||
from fastapi import HTTPException
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -57,17 +57,17 @@ class AiOpenai(BaseConnectorAi):
|
|||
connectorType="openai",
|
||||
maxTokens=128000,
|
||||
contextLength=128000,
|
||||
costPer1kTokens=0.03,
|
||||
costPer1kTokensInput=0.03,
|
||||
costPer1kTokensOutput=0.06,
|
||||
speedRating=8,
|
||||
qualityRating=9,
|
||||
capabilities=["text_generation", "chat", "reasoning", "analysis"],
|
||||
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.ANALYSIS],
|
||||
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.ANALYSIS],
|
||||
functionCall=self.callAiBasic,
|
||||
priority="balanced",
|
||||
processingMode="advanced",
|
||||
preferredFor=["general", "analyse_content"],
|
||||
version="gpt-4o"
|
||||
priority=PriorityEnum.BALANCED,
|
||||
processingMode=ProcessingModeEnum.ADVANCED,
|
||||
operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.ANALYSE],
|
||||
version="gpt-4o",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
|
||||
),
|
||||
AiModel(
|
||||
name="openai_callAiBasic_gpt35",
|
||||
|
|
@ -75,17 +75,17 @@ class AiOpenai(BaseConnectorAi):
|
|||
connectorType="openai",
|
||||
maxTokens=16000,
|
||||
contextLength=16000,
|
||||
costPer1kTokens=0.0015,
|
||||
costPer1kTokensInput=0.0015,
|
||||
costPer1kTokensOutput=0.002,
|
||||
speedRating=9,
|
||||
qualityRating=7,
|
||||
capabilities=["text_generation", "chat", "reasoning"],
|
||||
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.FAST, ModelTags.COST_EFFECTIVE],
|
||||
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING],
|
||||
functionCall=self.callAiBasic,
|
||||
priority="speed",
|
||||
processingMode="basic",
|
||||
preferredFor=["general"],
|
||||
version="gpt-3.5-turbo"
|
||||
priority=PriorityEnum.SPEED,
|
||||
processingMode=ProcessingModeEnum.BASIC,
|
||||
operationTypes=[OperationTypeEnum.GENERAL],
|
||||
version="gpt-3.5-turbo",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0015 + (bytesReceived / 4 / 1000) * 0.002
|
||||
),
|
||||
AiModel(
|
||||
name="openai_callAiImage",
|
||||
|
|
@ -93,17 +93,17 @@ class AiOpenai(BaseConnectorAi):
|
|||
connectorType="openai",
|
||||
maxTokens=128000,
|
||||
contextLength=128000,
|
||||
costPer1kTokens=0.03,
|
||||
costPer1kTokensInput=0.03,
|
||||
costPer1kTokensOutput=0.06,
|
||||
speedRating=7,
|
||||
qualityRating=9,
|
||||
capabilities=["image_analysis", "vision", "multimodal"],
|
||||
tags=[ModelTags.IMAGE, ModelTags.VISION, ModelTags.MULTIMODAL],
|
||||
capabilities=[ModelCapabilitiesEnum.IMAGE_ANALYSE, ModelCapabilitiesEnum.VISION, ModelCapabilitiesEnum.MULTIMODAL],
|
||||
functionCall=self.callAiImage,
|
||||
priority="quality",
|
||||
processingMode="detailed",
|
||||
preferredFor=["image_analysis"],
|
||||
version="gpt-4o"
|
||||
priority=PriorityEnum.QUALITY,
|
||||
processingMode=ProcessingModeEnum.DETAILED,
|
||||
operationTypes=[OperationTypeEnum.IMAGE_ANALYSE],
|
||||
version="gpt-4o",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
|
||||
),
|
||||
AiModel(
|
||||
name="openai_generateImage",
|
||||
|
|
@ -111,17 +111,17 @@ class AiOpenai(BaseConnectorAi):
|
|||
connectorType="openai",
|
||||
maxTokens=0, # Image generation doesn't use tokens
|
||||
contextLength=0,
|
||||
costPer1kTokens=0.04,
|
||||
costPer1kTokensInput=0.04,
|
||||
costPer1kTokensOutput=0.0,
|
||||
speedRating=6,
|
||||
qualityRating=9,
|
||||
capabilities=["image_generation", "art", "visual_creation"],
|
||||
tags=[ModelTags.IMAGE_GENERATION, ModelTags.ART, ModelTags.VISUAL],
|
||||
capabilities=[ModelCapabilitiesEnum.IMAGE_GENERATE, ModelCapabilitiesEnum.ART, ModelCapabilitiesEnum.VISUAL_CREATION],
|
||||
functionCall=self.generateImage,
|
||||
priority="quality",
|
||||
processingMode="detailed",
|
||||
preferredFor=["image_generation"],
|
||||
version="dall-e-3"
|
||||
priority=PriorityEnum.QUALITY,
|
||||
processingMode=ProcessingModeEnum.DETAILED,
|
||||
operationTypes=[OperationTypeEnum.IMAGE_GENERATE],
|
||||
version="dall-e-3",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from typing import Dict, Any, List, Union, Optional
|
|||
from fastapi import HTTPException
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -55,17 +55,17 @@ class AiPerplexity(BaseConnectorAi):
|
|||
connectorType="perplexity",
|
||||
maxTokens=128000,
|
||||
contextLength=128000,
|
||||
costPer1kTokens=0.005,
|
||||
costPer1kTokensInput=0.005,
|
||||
costPer1kTokensOutput=0.005,
|
||||
speedRating=8,
|
||||
qualityRating=8,
|
||||
capabilities=["text_generation", "chat", "reasoning", "web_search"],
|
||||
tags=[ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING, ModelTags.WEB, ModelTags.SEARCH, ModelTags.COST_EFFECTIVE],
|
||||
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.WEB_SEARCH],
|
||||
functionCall=self.callAiBasic,
|
||||
priority="balanced",
|
||||
processingMode="advanced",
|
||||
preferredFor=["general", "web_research"],
|
||||
version="llama-3.1-sonar-large-128k-online"
|
||||
priority=PriorityEnum.BALANCED,
|
||||
processingMode=ProcessingModeEnum.ADVANCED,
|
||||
operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.WEB_RESEARCH],
|
||||
version="llama-3.1-sonar-large-128k-online",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005
|
||||
),
|
||||
AiModel(
|
||||
name="perplexity_callAiWithWebSearch",
|
||||
|
|
@ -73,17 +73,17 @@ class AiPerplexity(BaseConnectorAi):
|
|||
connectorType="perplexity",
|
||||
maxTokens=128000,
|
||||
contextLength=128000,
|
||||
costPer1kTokens=0.01,
|
||||
costPer1kTokensInput=0.01,
|
||||
costPer1kTokensOutput=0.01,
|
||||
speedRating=7,
|
||||
qualityRating=9,
|
||||
capabilities=["text_generation", "web_search", "research"],
|
||||
tags=[ModelTags.TEXT, ModelTags.WEB, ModelTags.SEARCH, ModelTags.RESEARCH, ModelTags.HIGH_QUALITY],
|
||||
capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.RESEARCH],
|
||||
functionCall=self.callAiWithWebSearch,
|
||||
priority="quality",
|
||||
processingMode="detailed",
|
||||
preferredFor=["web_research"],
|
||||
version="sonar-pro"
|
||||
priority=PriorityEnum.QUALITY,
|
||||
processingMode=ProcessingModeEnum.DETAILED,
|
||||
operationTypes=[OperationTypeEnum.WEB_RESEARCH],
|
||||
version="sonar-pro",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01
|
||||
),
|
||||
AiModel(
|
||||
name="perplexity_researchTopic",
|
||||
|
|
@ -91,17 +91,17 @@ class AiPerplexity(BaseConnectorAi):
|
|||
connectorType="perplexity",
|
||||
maxTokens=32000,
|
||||
contextLength=32000,
|
||||
costPer1kTokens=0.002,
|
||||
costPer1kTokensInput=0.002,
|
||||
costPer1kTokensOutput=0.002,
|
||||
speedRating=8,
|
||||
qualityRating=8,
|
||||
capabilities=["web_search", "research", "information_gathering"],
|
||||
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.RESEARCH, ModelTags.INFORMATION, ModelTags.COST_EFFECTIVE],
|
||||
capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.RESEARCH, ModelCapabilitiesEnum.INFORMATION_GATHERING],
|
||||
functionCall=self.researchTopic,
|
||||
priority="cost",
|
||||
processingMode="basic",
|
||||
preferredFor=["web_research"],
|
||||
version="mistral-7b-instruct"
|
||||
priority=PriorityEnum.COST,
|
||||
processingMode=ProcessingModeEnum.BASIC,
|
||||
operationTypes=[OperationTypeEnum.WEB_RESEARCH],
|
||||
version="mistral-7b-instruct",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
|
||||
),
|
||||
AiModel(
|
||||
name="perplexity_answerQuestion",
|
||||
|
|
@ -109,17 +109,17 @@ class AiPerplexity(BaseConnectorAi):
|
|||
connectorType="perplexity",
|
||||
maxTokens=32000,
|
||||
contextLength=32000,
|
||||
costPer1kTokens=0.002,
|
||||
costPer1kTokensInput=0.002,
|
||||
costPer1kTokensOutput=0.002,
|
||||
speedRating=8,
|
||||
qualityRating=8,
|
||||
capabilities=["web_search", "question_answering", "research"],
|
||||
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.RESEARCH, ModelTags.COST_EFFECTIVE],
|
||||
capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.QUESTION_ANSWERING, ModelCapabilitiesEnum.RESEARCH],
|
||||
functionCall=self.answerQuestion,
|
||||
priority="cost",
|
||||
processingMode="basic",
|
||||
preferredFor=["web_research"],
|
||||
version="mistral-7b-instruct"
|
||||
priority=PriorityEnum.COST,
|
||||
processingMode=ProcessingModeEnum.BASIC,
|
||||
operationTypes=[OperationTypeEnum.WEB_RESEARCH],
|
||||
version="mistral-7b-instruct",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
|
||||
),
|
||||
AiModel(
|
||||
name="perplexity_getCurrentNews",
|
||||
|
|
@ -127,17 +127,17 @@ class AiPerplexity(BaseConnectorAi):
|
|||
connectorType="perplexity",
|
||||
maxTokens=32000,
|
||||
contextLength=32000,
|
||||
costPer1kTokens=0.002,
|
||||
costPer1kTokensInput=0.002,
|
||||
costPer1kTokensOutput=0.002,
|
||||
speedRating=8,
|
||||
qualityRating=8,
|
||||
capabilities=["web_search", "news", "current_events"],
|
||||
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.COST_EFFECTIVE],
|
||||
capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.NEWS, ModelCapabilitiesEnum.CURRENT_EVENTS],
|
||||
functionCall=self.getCurrentNews,
|
||||
priority="cost",
|
||||
processingMode="basic",
|
||||
preferredFor=["web_research"],
|
||||
version="mistral-7b-instruct"
|
||||
priority=PriorityEnum.COST,
|
||||
processingMode=ProcessingModeEnum.BASIC,
|
||||
operationTypes=[OperationTypeEnum.WEB_RESEARCH],
|
||||
version="mistral-7b-instruct",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from tavily import AsyncTavilyClient
|
|||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.shared.timezoneUtils import get_utc_timestamp
|
||||
from modules.aicore.aicoreBase import BaseConnectorAi
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelTags
|
||||
from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum
|
||||
from modules.datamodels.datamodelWeb import (
|
||||
WebSearchActionResult,
|
||||
WebSearchActionDocument,
|
||||
|
|
@ -46,9 +46,9 @@ class ConnectorWeb(BaseConnectorAi):
|
|||
super().__init__()
|
||||
self.client: Optional[AsyncTavilyClient] = None
|
||||
# Cached settings loaded at initialization time
|
||||
self.crawl_timeout: int = 30
|
||||
self.crawl_max_retries: int = 3
|
||||
self.crawl_retry_delay: int = 2
|
||||
self.crawlTimeout: int = 30
|
||||
self.crawlMaxRetries: int = 3
|
||||
self.crawlRetryDelay: int = 2
|
||||
# Cached web search constraints (camelCase per project style)
|
||||
self.webSearchMinResults: int = 1
|
||||
self.webSearchMaxResults: int = 20
|
||||
|
|
@ -66,17 +66,17 @@ class ConnectorWeb(BaseConnectorAi):
|
|||
connectorType="tavily",
|
||||
maxTokens=0, # Web search doesn't use tokens
|
||||
contextLength=0,
|
||||
costPer1kTokens=0.0,
|
||||
costPer1kTokensInput=0.0,
|
||||
costPer1kTokensOutput=0.0,
|
||||
speedRating=8,
|
||||
qualityRating=8,
|
||||
capabilities=["web_search", "information_retrieval", "url_discovery"],
|
||||
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.INFORMATION],
|
||||
capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.INFORMATION_RETRIEVAL, ModelCapabilitiesEnum.URL_DISCOVERY],
|
||||
functionCall=self.search,
|
||||
priority="balanced",
|
||||
processingMode="basic",
|
||||
preferredFor=["web_research"],
|
||||
version="tavily-search"
|
||||
priority=PriorityEnum.BALANCED,
|
||||
processingMode=ProcessingModeEnum.BASIC,
|
||||
operationTypes=[OperationTypeEnum.WEB_RESEARCH],
|
||||
version="tavily-search",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived, searchDepth="basic", numRequests=1: numRequests * (1 if searchDepth == "basic" else 2) * 0.008
|
||||
),
|
||||
AiModel(
|
||||
name="tavily_extract",
|
||||
|
|
@ -84,17 +84,17 @@ class ConnectorWeb(BaseConnectorAi):
|
|||
connectorType="tavily",
|
||||
maxTokens=0, # Web extraction doesn't use tokens
|
||||
contextLength=0,
|
||||
costPer1kTokens=0.0,
|
||||
costPer1kTokensInput=0.0,
|
||||
costPer1kTokensOutput=0.0,
|
||||
speedRating=6,
|
||||
qualityRating=8,
|
||||
capabilities=["web_crawling", "content_extraction", "text_extraction"],
|
||||
tags=[ModelTags.WEB, ModelTags.EXTRACT, ModelTags.CONTENT],
|
||||
capabilities=[ModelCapabilitiesEnum.WEB_CRAWLING, ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.TEXT_EXTRACTION],
|
||||
functionCall=self.crawl,
|
||||
priority="balanced",
|
||||
processingMode="basic",
|
||||
preferredFor=["web_research"],
|
||||
version="tavily-extract"
|
||||
priority=PriorityEnum.BALANCED,
|
||||
processingMode=ProcessingModeEnum.BASIC,
|
||||
operationTypes=[OperationTypeEnum.WEB_RESEARCH],
|
||||
version="tavily-extract",
|
||||
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived, extractionDepth="basic", numSuccessfulUrls=1: (numSuccessfulUrls / 5) * (1 if extractionDepth == "basic" else 2) * 0.008
|
||||
),
|
||||
AiModel(
|
||||
name="tavily_crawl",
|
||||
|
|
@ -102,17 +102,17 @@ class ConnectorWeb(BaseConnectorAi):
|
|||
connectorType="tavily",
|
||||
maxTokens=0, # Web crawling doesn't use tokens
|
||||
contextLength=0,
|
||||
costPer1kTokens=0.0,
|
||||
costPer1kTokensInput=0.0,
|
||||
costPer1kTokensOutput=0.0,
|
||||
speedRating=6,
|
||||
qualityRating=8,
|
||||
capabilities=["web_crawling", "content_extraction", "mapping"],
|
||||
tags=[ModelTags.WEB, ModelTags.CRAWL, ModelTags.EXTRACT],
|
||||
capabilities=[ModelCapabilitiesEnum.WEB_CRAWLING, ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.MAPPING],
|
||||
functionCall=self.crawl,
|
||||
priority="balanced",
|
||||
processingMode="basic",
|
||||
preferredFor=["web_research"],
|
||||
version="tavily-crawl"
|
||||
priority=PriorityEnum.BALANCED,
|
||||
processingMode=ProcessingModeEnum.BASIC,
|
||||
operationTypes=[OperationTypeEnum.WEB_RESEARCH],
|
||||
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(
|
||||
name="tavily_scrape",
|
||||
|
|
@ -120,17 +120,17 @@ class ConnectorWeb(BaseConnectorAi):
|
|||
connectorType="tavily",
|
||||
maxTokens=0, # Web scraping doesn't use tokens
|
||||
contextLength=0,
|
||||
costPer1kTokens=0.0,
|
||||
costPer1kTokensInput=0.0,
|
||||
costPer1kTokensOutput=0.0,
|
||||
speedRating=6,
|
||||
qualityRating=8,
|
||||
capabilities=["web_search", "web_crawling", "content_extraction", "information_retrieval"],
|
||||
tags=[ModelTags.WEB, ModelTags.SEARCH, ModelTags.CRAWL, ModelTags.EXTRACT, ModelTags.CONTENT, ModelTags.INFORMATION],
|
||||
capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.WEB_CRAWLING, ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.INFORMATION_RETRIEVAL],
|
||||
functionCall=self.scrape,
|
||||
priority="balanced",
|
||||
processingMode="basic",
|
||||
preferredFor=["web_research"],
|
||||
version="tavily-search-extract"
|
||||
priority=PriorityEnum.BALANCED,
|
||||
processingMode=ProcessingModeEnum.BASIC,
|
||||
operationTypes=[OperationTypeEnum.WEB_RESEARCH],
|
||||
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:
|
||||
raise ValueError("Tavily API key not configured. Please set Connector_WebTavily_API_KEY_SECRET in config.ini")
|
||||
# Load and cache web crawl related configuration
|
||||
crawl_timeout = int(APP_CONFIG.get("Web_Crawl_TIMEOUT", "30"))
|
||||
crawl_max_retries = int(APP_CONFIG.get("Web_Crawl_MAX_RETRIES", "3"))
|
||||
crawl_retry_delay = int(APP_CONFIG.get("Web_Crawl_RETRY_DELAY", "2"))
|
||||
crawlTimeout = int(APP_CONFIG.get("Web_Crawl_TIMEOUT", "30"))
|
||||
crawlMaxRetries = int(APP_CONFIG.get("Web_Crawl_MAX_RETRIES", "3"))
|
||||
crawlRetryDelay = int(APP_CONFIG.get("Web_Crawl_RETRY_DELAY", "2"))
|
||||
return cls(
|
||||
client=AsyncTavilyClient(api_key=api_key),
|
||||
crawl_timeout=crawl_timeout,
|
||||
crawl_max_retries=crawl_max_retries,
|
||||
crawl_retry_delay=crawl_retry_delay,
|
||||
crawlTimeout=crawlTimeout,
|
||||
crawlMaxRetries=crawlMaxRetries,
|
||||
crawlRetryDelay=crawlRetryDelay,
|
||||
webSearchMinResults=int(APP_CONFIG.get("Web_Search_MIN_RESULTS", "1")),
|
||||
webSearchMaxResults=int(APP_CONFIG.get("Web_Search_MAX_RESULTS", "20")),
|
||||
)
|
||||
|
|
@ -363,10 +363,10 @@ class ConnectorWeb(BaseConnectorAi):
|
|||
) -> list[WebSearchResult]:
|
||||
"""Calls the Tavily API to perform a web search."""
|
||||
# Make sure max_results is within the allowed range (use cached values)
|
||||
min_results = self.webSearchMinResults
|
||||
max_allowed_results = self.webSearchMaxResults
|
||||
if max_results < min_results or max_results > max_allowed_results:
|
||||
raise ValueError(f"max_results must be between {min_results} and {max_allowed_results}")
|
||||
minResults = self.webSearchMinResults
|
||||
maxAllowedResults = self.webSearchMaxResults
|
||||
if max_results < minResults or max_results > maxAllowedResults:
|
||||
raise ValueError(f"max_results must be between {minResults} and {maxAllowedResults}")
|
||||
|
||||
# Perform actual API call
|
||||
# Build kwargs only for provided options to avoid API rejections
|
||||
|
|
@ -409,16 +409,16 @@ class ConnectorWeb(BaseConnectorAi):
|
|||
format: str | None = None,
|
||||
) -> list[WebCrawlResult]:
|
||||
"""Calls the Tavily API to extract text content from URLs with retry logic."""
|
||||
max_retries = self.crawl_max_retries
|
||||
retry_delay = self.crawl_retry_delay
|
||||
timeout = self.crawl_timeout
|
||||
maxRetries = self.crawlMaxRetries
|
||||
retryDelay = self.crawlRetryDelay
|
||||
timeout = self.crawlTimeout
|
||||
|
||||
logger.debug(f"Starting crawl of {len(urls)} URLs: {urls}")
|
||||
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:
|
||||
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
|
||||
# Build kwargs for extract
|
||||
|
|
@ -460,11 +460,11 @@ class ConnectorWeb(BaseConnectorAi):
|
|||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"Crawl attempt {attempt + 1} timed out after {timeout} seconds for URLs: {urls}")
|
||||
if attempt < max_retries:
|
||||
logger.info(f"Retrying in {retry_delay} seconds...")
|
||||
await asyncio.sleep(retry_delay)
|
||||
if attempt < maxRetries:
|
||||
logger.info(f"Retrying in {retryDelay} seconds...")
|
||||
await asyncio.sleep(retryDelay)
|
||||
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:
|
||||
logger.warning(f"Crawl attempt {attempt + 1} failed for URLs {urls}: {str(e)}")
|
||||
|
|
@ -483,8 +483,8 @@ class ConnectorWeb(BaseConnectorAi):
|
|||
if len(url) > 2000:
|
||||
logger.debug(f" WARNING: URL is very long ({len(url)} chars)")
|
||||
|
||||
if attempt < max_retries:
|
||||
logger.info(f"Retrying in {retry_delay} seconds...")
|
||||
await asyncio.sleep(retry_delay)
|
||||
if attempt < maxRetries:
|
||||
logger.info(f"Retrying in {retryDelay} seconds...")
|
||||
await asyncio.sleep(retryDelay)
|
||||
else:
|
||||
raise Exception(f"Crawl failed after {max_retries + 1} attempts: {str(e)}")
|
||||
raise Exception(f"Crawl failed after {maxRetries + 1} attempts: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -1,86 +1,65 @@
|
|||
from typing import Optional, List, Dict, Any, Literal, Callable
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from enum import Enum
|
||||
|
||||
# Operation Types
|
||||
class OperationType:
|
||||
class OperationTypeEnum(str, Enum):
|
||||
GENERAL = "general"
|
||||
GENERATE_PLAN = "generate_plan"
|
||||
ANALYSE_CONTENT = "analyse_content"
|
||||
GENERATE_CONTENT = "generate_content"
|
||||
WEB_RESEARCH = "web_research"
|
||||
IMAGE_ANALYSIS = "image_analysis"
|
||||
IMAGE_GENERATION = "image_generation"
|
||||
PLAN = "plan"
|
||||
ANALYSE = "analyse"
|
||||
GENERATE = "generate"
|
||||
WEB_RESEARCH = "webResearch"
|
||||
IMAGE_ANALYSE = "imageAnalyse"
|
||||
IMAGE_GENERATE = "imageGenerate"
|
||||
|
||||
|
||||
# Processing Modes
|
||||
class ProcessingMode:
|
||||
class ProcessingModeEnum(str, Enum):
|
||||
BASIC = "basic"
|
||||
ADVANCED = "advanced"
|
||||
DETAILED = "detailed"
|
||||
|
||||
|
||||
# Priority Levels
|
||||
class Priority:
|
||||
class PriorityEnum(str, Enum):
|
||||
SPEED = "speed"
|
||||
QUALITY = "quality"
|
||||
COST = "cost"
|
||||
BALANCED = "balanced"
|
||||
|
||||
|
||||
# Model Tags
|
||||
class ModelTags:
|
||||
# Core capabilities
|
||||
TEXT = "text"
|
||||
# Model Capabilities Enumeration
|
||||
class ModelCapabilitiesEnum(str, Enum):
|
||||
# Text generation capabilities
|
||||
TEXT_GENERATION = "text_generation"
|
||||
CHAT = "chat"
|
||||
REASONING = "reasoning"
|
||||
ANALYSIS = "analysis"
|
||||
IMAGE = "image"
|
||||
|
||||
# Image capabilities
|
||||
IMAGE_ANALYSE = "imageAnalyse"
|
||||
IMAGE_GENERATE = "imageGenerate"
|
||||
VISION = "vision"
|
||||
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"
|
||||
VISUAL = "visual"
|
||||
VARIATIONS = "variations"
|
||||
API = "api"
|
||||
INFO = "info"
|
||||
MODELS = "models"
|
||||
|
||||
|
||||
# Operation Type to Required Tags Mapping
|
||||
OPERATION_TAG_MAPPING = {
|
||||
OperationType.GENERAL: [ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING],
|
||||
OperationType.GENERATE_PLAN: [ModelTags.TEXT, ModelTags.REASONING, ModelTags.ANALYSIS],
|
||||
OperationType.ANALYSE_CONTENT: [ModelTags.TEXT, ModelTags.ANALYSIS, ModelTags.REASONING],
|
||||
OperationType.GENERATE_CONTENT: [ModelTags.TEXT, ModelTags.CHAT, ModelTags.REASONING],
|
||||
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,
|
||||
}
|
||||
|
||||
VISUAL_CREATION = "visual_creation"
|
||||
|
||||
# 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"
|
||||
|
||||
# Research capabilities
|
||||
RESEARCH = "research"
|
||||
QUESTION_ANSWERING = "question_answering"
|
||||
INFORMATION_GATHERING = "information_gathering"
|
||||
NEWS = "news"
|
||||
CURRENT_EVENTS = "current_events"
|
||||
|
||||
|
||||
class AiModel(BaseModel):
|
||||
"""Enhanced AI model definition with dynamic capabilities."""
|
||||
|
||||
|
|
@ -94,75 +73,67 @@ class AiModel(BaseModel):
|
|||
contextLength: int = Field(description="Maximum context length this model can handle")
|
||||
|
||||
# Cost information
|
||||
costPer1kTokens: float = Field(default=0.0, description="Cost per 1000 input tokens")
|
||||
costPer1kTokensInput: float = Field(default=0.0, description="Cost per 1000 input tokens")
|
||||
costPer1kTokensOutput: float = Field(default=0.0, description="Cost per 1000 output tokens")
|
||||
|
||||
# Performance ratings
|
||||
speedRating: int = Field(ge=1, le=10, description="Speed rating (1-10, higher = faster)")
|
||||
qualityRating: int = Field(ge=1, le=10, description="Quality rating (1-10, higher = better)")
|
||||
|
||||
# Capabilities and tags
|
||||
capabilities: List[str] = Field(description="List of model capabilities")
|
||||
tags: List[str] = Field(description="List of model tags for filtering")
|
||||
|
||||
|
||||
# Function reference (not serialized)
|
||||
functionCall: Optional[Callable] = Field(default=None, exclude=True, description="Function to call for this model")
|
||||
calculatePriceUsd: Optional[Callable] = Field(default=None, exclude=True, description="Function to calculate price in USD")
|
||||
|
||||
# Selection criteria
|
||||
priority: str = Field(default="balanced", description="Default priority for this model")
|
||||
processingMode: str = Field(default="basic", description="Default processing mode")
|
||||
isAvailable: bool = Field(default=True, description="Whether model is currently available")
|
||||
|
||||
# Advanced selection criteria
|
||||
capabilities: List[ModelCapabilitiesEnum] = Field(description="List of model capabilities. See ModelCapabilitiesEnum enum for available values.")
|
||||
priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Default priority for this model. See PriorityEnum for available values.")
|
||||
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")
|
||||
minContextLength: Optional[int] = Field(default=None, description="Minimum context length required")
|
||||
maxCost: Optional[float] = Field(default=None, description="Maximum cost this model should be used for")
|
||||
preferredFor: List[str] = Field(default=[], description="Operation types this model is preferred for")
|
||||
avoidFor: List[str] = Field(default=[], description="Operation types this model should avoid")
|
||||
isAvailable: bool = Field(default=True, description="Whether model is currently available")
|
||||
|
||||
# Metadata
|
||||
version: Optional[str] = Field(default=None, description="Model version")
|
||||
lastUpdated: Optional[str] = Field(default=None, description="Last update timestamp")
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True # Allow Callable type
|
||||
arbitraryTypesAllowed = True # Allow Callable type
|
||||
|
||||
|
||||
class ModelCapabilities(BaseModel):
|
||||
"""Model capabilities and characteristics for dynamic selection."""
|
||||
|
||||
name: str = Field(description="Model name/identifier")
|
||||
maxTokens: int = Field(description="Maximum token limit for this model")
|
||||
capabilities: List[str] = Field(description="List of capabilities: text, image, vision, reasoning, analysis, etc.")
|
||||
costPerToken: float = Field(default=0.0, description="Cost per token (if available)")
|
||||
processingTime: float = Field(default=1.0, description="Average processing time multiplier")
|
||||
isAvailable: bool = Field(default=True, description="Whether model is currently available")
|
||||
class SelectionRule(BaseModel):
|
||||
"""A rule for model selection."""
|
||||
name: str = Field(description="Rule name identifier")
|
||||
condition: str = Field(description="Description of when this rule applies")
|
||||
weight: float = Field(description="Weight for scoring (higher = more important)")
|
||||
operationTypes: List[OperationTypeEnum] = Field(description="Operation types this rule applies to")
|
||||
priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Priority level for this rule")
|
||||
capabilities: List[ModelCapabilitiesEnum] = Field(default=[], description="Required capabilities for this rule")
|
||||
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):
|
||||
"""Options for centralized AI processing with clear operation types and tags."""
|
||||
|
||||
operationType: str = Field(default="general", description="Type of operation: general, generate_plan, analyse_content, generate_content, web_research")
|
||||
priority: str = Field(default="balanced", description="speed|quality|cost|balanced")
|
||||
operationType: OperationTypeEnum = Field(default=OperationTypeEnum.GENERAL, description="Type of operation")
|
||||
priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Priority level")
|
||||
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")
|
||||
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")
|
||||
maxCost: Optional[float] = Field(default=None, description="Max cost budget")
|
||||
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: str = Field(default="basic", description="Processing mode: basic, advanced, detailed")
|
||||
processingMode: ProcessingModeEnum = Field(default=ProcessingModeEnum.BASIC, description="Processing mode")
|
||||
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)")
|
||||
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
|
||||
temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0, description="Temperature for response generation (0.0-2.0, lower = more consistent)")
|
||||
maxTokens: Optional[int] = Field(default=None, ge=1, le=32000, description="Maximum tokens in response")
|
||||
maxParts: Optional[int] = Field(default=1000, ge=1, le=1000, description="Maximum number of continuation parts to fetch")
|
||||
|
||||
|
||||
|
||||
class AiCallRequest(BaseModel):
|
||||
"""Centralized AI call request payload for interface use."""
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,19 +6,14 @@ import time
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# No more hardcoded imports - everything is discovered dynamically
|
||||
from modules.aicore.aicoreModelRegistry import model_registry
|
||||
from modules.aicore.aicoreModelSelector import model_selector
|
||||
from modules.datamodels.datamodelAi import (
|
||||
AiModel,
|
||||
AiCallOptions,
|
||||
AiCallRequest,
|
||||
AiCallResponse,
|
||||
OperationType,
|
||||
ProcessingMode,
|
||||
Priority,
|
||||
ModelTags,
|
||||
OPERATION_TAG_MAPPING,
|
||||
PROCESSING_MODE_PRIORITY_MAPPING
|
||||
OperationTypeEnum,
|
||||
)
|
||||
from modules.datamodels.datamodelWeb import (
|
||||
WebResearchRequest,
|
||||
|
|
@ -47,14 +42,14 @@ class AiObjects:
|
|||
logger.info("Auto-discovering AI connectors...")
|
||||
|
||||
# Use the model registry's built-in discovery mechanism
|
||||
discovered_connectors = model_registry.discoverConnectors()
|
||||
discoveredConnectors = model_registry.discoverConnectors()
|
||||
|
||||
# Register each discovered connector
|
||||
for connector in discovered_connectors:
|
||||
for connector in discoveredConnectors:
|
||||
model_registry.registerConnector(connector)
|
||||
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")
|
||||
|
||||
@classmethod
|
||||
|
|
@ -68,25 +63,25 @@ class AiObjects:
|
|||
def _selectModel(self, prompt: str, context: str, options: AiCallOptions) -> str:
|
||||
"""Select the best model using dynamic model selection system."""
|
||||
# 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")
|
||||
raise ValueError("No AI models available")
|
||||
|
||||
# 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")
|
||||
raise ValueError("No suitable AI model found")
|
||||
|
||||
logger.info(f"Selected model: {selected_model.name} ({selected_model.displayName})")
|
||||
return selected_model.name
|
||||
logger.info(f"Selected model: {selectedModel.name} ({selectedModel.displayName})")
|
||||
return selectedModel.name
|
||||
|
||||
|
||||
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
|
||||
context = request.context or ""
|
||||
|
|
@ -96,192 +91,247 @@ class AiObjects:
|
|||
inputBytes = len((prompt + context).encode("utf-8"))
|
||||
|
||||
# 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")
|
||||
if len(data) <= limit:
|
||||
return text
|
||||
return data[:limit].decode("utf-8", errors="ignore") + "... [truncated]"
|
||||
|
||||
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:
|
||||
context = maybeTruncate(context, 70000)
|
||||
context = _maybeTruncate(context, 70000)
|
||||
|
||||
# Derive generation parameters
|
||||
temperature = getattr(options, "temperature", None)
|
||||
if temperature is None:
|
||||
temperature = 0.2
|
||||
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
|
||||
|
||||
|
||||
try:
|
||||
# Select the best model using dynamic selection
|
||||
modelName = self._selectModel(prompt, context, options)
|
||||
selectedModel = model_registry.getModel(modelName)
|
||||
|
||||
if not selectedModel:
|
||||
raise ValueError(f"Selected model {modelName} not found in registry")
|
||||
# Replace <TOKEN_LIMIT> placeholder in prompt for this specific model
|
||||
context_length = selectedModel.contextLength
|
||||
if context_length > 0:
|
||||
token_limit = str(context_length)
|
||||
else:
|
||||
token_limit = "16000" # Default for text generation
|
||||
|
||||
# Create a copy of the prompt for this model call
|
||||
modelPrompt = prompt
|
||||
if "<TOKEN_LIMIT>" in modelPrompt:
|
||||
modelPrompt = modelPrompt.replace("<TOKEN_LIMIT>", token_limit)
|
||||
logger.debug(f"Replaced <TOKEN_LIMIT> with {token_limit} for model {modelName}")
|
||||
|
||||
# Update messages array with replaced content
|
||||
messages = []
|
||||
if context:
|
||||
messages.append({"role": "system", "content": f"Context from documents:\n{context}"})
|
||||
messages.append({"role": "user", "content": modelPrompt})
|
||||
|
||||
# Start timing
|
||||
startTime = time.time()
|
||||
|
||||
# Get the connector for this model
|
||||
connector = model_registry.getConnectorForModel(modelName)
|
||||
if not connector:
|
||||
raise ValueError(f"No connector found for model {modelName}")
|
||||
|
||||
# Call the model's function directly
|
||||
if selectedModel.functionCall:
|
||||
# Use the model's function call directly
|
||||
if modelName.startswith("perplexity_callAiWithWebSearch"):
|
||||
query = modelPrompt
|
||||
if context:
|
||||
query = f"Context: {context}\n\nQuery: {modelPrompt}"
|
||||
content = await selectedModel.functionCall(query, temperature=temperature, maxTokens=maxTokens)
|
||||
elif modelName.startswith("perplexity_researchTopic"):
|
||||
content = await selectedModel.functionCall(modelPrompt)
|
||||
elif modelName.startswith("perplexity_answerQuestion"):
|
||||
content = await selectedModel.functionCall(modelPrompt, context)
|
||||
elif modelName.startswith("perplexity_getCurrentNews"):
|
||||
content = await selectedModel.functionCall(modelPrompt)
|
||||
else:
|
||||
# Standard callAiBasic
|
||||
if selectedModel.connectorType == "anthropic":
|
||||
response = await selectedModel.functionCall(messages, temperature=temperature, maxTokens=maxTokens)
|
||||
content = response["choices"][0]["message"]["content"]
|
||||
else:
|
||||
content = await selectedModel.functionCall(messages, temperature=temperature, maxTokens=maxTokens)
|
||||
else:
|
||||
raise ValueError(f"Model {modelName} has no function call defined")
|
||||
|
||||
# Calculate timing and output bytes
|
||||
endTime = time.time()
|
||||
processingTime = endTime - startTime
|
||||
outputBytes = len(content.encode("utf-8"))
|
||||
|
||||
# Calculate price using model's cost information
|
||||
estimated_tokens = inputBytes / 4
|
||||
priceUsd = (estimated_tokens / 1000) * selectedModel.costPer1kTokens + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
|
||||
|
||||
logger.info(f"✅ AI call successful with model: {modelName}")
|
||||
logger.info(f" Processing time: {processingTime:.2f}s")
|
||||
logger.info(f" Input: {inputBytes} bytes, Output: {outputBytes} bytes")
|
||||
logger.info(f" Estimated cost: ${priceUsd:.4f}")
|
||||
|
||||
# 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(
|
||||
success=True,
|
||||
content=content,
|
||||
model=modelName,
|
||||
processingTime=processingTime,
|
||||
priceUsd=priceUsd,
|
||||
bytesSent=inputBytes,
|
||||
bytesReceived=outputBytes,
|
||||
errorCount=0
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI call failed: {e}")
|
||||
return AiCallResponse(
|
||||
success=False,
|
||||
content=f"AI call failed: {str(e)}",
|
||||
model="none",
|
||||
processingTime=0.0,
|
||||
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:
|
||||
logger.info(f"Attempting AI call with model: {model.name} (attempt {attempt + 1}/{len(fallbackModels)})")
|
||||
|
||||
# Call the model
|
||||
response = await self._callWithModel(model, prompt, context, temperature, maxTokens, inputBytes)
|
||||
|
||||
logger.info(f"✅ AI call successful with model: {model.name}")
|
||||
return response
|
||||
|
||||
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:
|
||||
# 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
|
||||
modelPrompt = prompt
|
||||
if "<TOKEN_LIMIT>" in modelPrompt:
|
||||
modelPrompt = modelPrompt.replace("<TOKEN_LIMIT>", tokenLimit)
|
||||
logger.debug(f"Replaced <TOKEN_LIMIT> with {tokenLimit} for model {model.name}")
|
||||
|
||||
# Update messages array with replaced content
|
||||
messages = []
|
||||
if context:
|
||||
messages.append({"role": "system", "content": f"Context from documents:\n{context}"})
|
||||
messages.append({"role": "user", "content": modelPrompt})
|
||||
|
||||
# Start timing
|
||||
startTime = time.time()
|
||||
|
||||
# Get the connector for this model
|
||||
connector = model_registry.getConnectorForModel(model.name)
|
||||
if not connector:
|
||||
raise ValueError(f"No connector found for model {model.name}")
|
||||
|
||||
# Call the model's function directly
|
||||
if model.functionCall:
|
||||
# Use the model's function call directly
|
||||
if model.name.startswith("perplexity_callAiWithWebSearch"):
|
||||
query = modelPrompt
|
||||
if context:
|
||||
query = f"Context: {context}\n\nQuery: {modelPrompt}"
|
||||
content = await model.functionCall(query, temperature=temperature, maxTokens=maxTokens)
|
||||
elif model.name.startswith("perplexity_researchTopic"):
|
||||
content = await model.functionCall(modelPrompt)
|
||||
elif model.name.startswith("perplexity_answerQuestion"):
|
||||
content = await model.functionCall(modelPrompt, context)
|
||||
elif model.name.startswith("perplexity_getCurrentNews"):
|
||||
content = await model.functionCall(modelPrompt)
|
||||
else:
|
||||
# Standard callAiBasic
|
||||
if model.connectorType == "anthropic":
|
||||
response = await model.functionCall(messages, temperature=temperature, maxTokens=maxTokens)
|
||||
content = response["choices"][0]["message"]["content"]
|
||||
else:
|
||||
content = await model.functionCall(messages, temperature=temperature, maxTokens=maxTokens)
|
||||
else:
|
||||
raise ValueError(f"Model {model.name} has no function call defined")
|
||||
|
||||
# Calculate timing and output bytes
|
||||
endTime = time.time()
|
||||
processingTime = endTime - startTime
|
||||
outputBytes = len(content.encode("utf-8"))
|
||||
|
||||
# Calculate price using model's cost information
|
||||
priceUsd = model.costPer1kTokensInput * (inputBytes / 4 / 1000) + model.costPer1kTokensOutput * (outputBytes / 4 / 1000)
|
||||
|
||||
return AiCallResponse(
|
||||
content=content,
|
||||
modelName=model.name,
|
||||
priceUsd=priceUsd,
|
||||
processingTime=processingTime,
|
||||
bytesSent=inputBytes,
|
||||
bytesReceived=outputBytes,
|
||||
errorCount=0
|
||||
)
|
||||
|
||||
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."""
|
||||
|
||||
if options is None:
|
||||
options = AiCallOptions(operationType=OperationType.IMAGE_ANALYSIS)
|
||||
options = AiCallOptions(operationType=OperationTypeEnum.IMAGE_ANALYSE)
|
||||
|
||||
# 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"))
|
||||
|
||||
try:
|
||||
# Select the best model for image analysis
|
||||
modelName = self._selectModel(prompt, "", options)
|
||||
selectedModel = model_registry.getModel(modelName)
|
||||
|
||||
if not selectedModel:
|
||||
raise ValueError(f"Selected model {modelName} not found in registry")
|
||||
|
||||
# Get the connector for this model
|
||||
connector = model_registry.getConnectorForModel(modelName)
|
||||
if not connector:
|
||||
raise ValueError(f"No connector found for model {modelName}")
|
||||
|
||||
# Start timing
|
||||
startTime = time.time()
|
||||
|
||||
# Call the model's function directly
|
||||
if selectedModel.functionCall:
|
||||
content = await selectedModel.functionCall(prompt, imageData, mimeType)
|
||||
else:
|
||||
raise ValueError(f"Model {modelName} has no function call defined")
|
||||
|
||||
# Calculate timing and output bytes
|
||||
endTime = time.time()
|
||||
processingTime = endTime - startTime
|
||||
outputBytes = len(content.encode("utf-8"))
|
||||
|
||||
# Calculate price using model's cost information
|
||||
estimated_tokens = inputBytes / 4
|
||||
priceUsd = (estimated_tokens / 1000) * selectedModel.costPer1kTokens + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
|
||||
|
||||
logger.info(f"✅ Image analysis successful with model: {modelName}")
|
||||
# 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(
|
||||
success=True,
|
||||
content=content,
|
||||
model=modelName,
|
||||
processingTime=processingTime,
|
||||
priceUsd=priceUsd,
|
||||
bytesSent=inputBytes,
|
||||
bytesReceived=outputBytes,
|
||||
errorCount=0
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Image analysis failed: {e}")
|
||||
return AiCallResponse(
|
||||
success=False,
|
||||
content=f"Image analysis failed: {str(e)}",
|
||||
model="none",
|
||||
processingTime=0.0,
|
||||
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:
|
||||
logger.info(f"Attempting image analysis with model: {model.name} (attempt {attempt + 1}/{len(fallbackModels)})")
|
||||
|
||||
# Call the model
|
||||
response = await self._callImageWithModel(model, prompt, imageData, mimeType, inputBytes)
|
||||
|
||||
logger.info(f"✅ Image analysis successful with model: {model.name}")
|
||||
return response
|
||||
|
||||
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
|
||||
startTime = time.time()
|
||||
|
||||
# Call the model's function directly
|
||||
if model.functionCall:
|
||||
content = await model.functionCall(prompt, imageData, mimeType)
|
||||
else:
|
||||
raise ValueError(f"Model {model.name} has no function call defined")
|
||||
|
||||
# Calculate timing and output bytes
|
||||
endTime = time.time()
|
||||
processingTime = endTime - startTime
|
||||
outputBytes = len(content.encode("utf-8"))
|
||||
|
||||
# Calculate price using model's cost information
|
||||
priceUsd = model.costPer1kTokensInput * (inputBytes / 4 / 1000) + model.costPer1kTokensOutput * (outputBytes / 4 / 1000)
|
||||
|
||||
return AiCallResponse(
|
||||
content=content,
|
||||
modelName=model.name,
|
||||
priceUsd=priceUsd,
|
||||
processingTime=processingTime,
|
||||
bytesSent=inputBytes,
|
||||
bytesReceived=outputBytes,
|
||||
errorCount=0
|
||||
)
|
||||
|
||||
async def generateImage(self, prompt: str, size: str = "1024x1024", quality: str = "standard", style: str = "vivid", options: AiCallOptions = None) -> AiCallResponse:
|
||||
"""Generate an image using AI."""
|
||||
|
||||
if options is None:
|
||||
options = AiCallOptions(operationType=OperationType.IMAGE_GENERATION)
|
||||
options = AiCallOptions(operationType=OperationTypeEnum.IMAGE_GENERATE)
|
||||
|
||||
# Calculate input bytes
|
||||
inputBytes = len(prompt.encode("utf-8"))
|
||||
|
|
@ -315,8 +365,8 @@ class AiObjects:
|
|||
outputBytes = len(content.encode("utf-8"))
|
||||
|
||||
# Calculate price using model's cost information
|
||||
estimated_tokens = inputBytes / 4
|
||||
priceUsd = (estimated_tokens / 1000) * selectedModel.costPer1kTokens + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
|
||||
estimatedTokens = inputBytes / 4
|
||||
priceUsd = (estimatedTokens / 1000) * selectedModel.costPer1kTokensInput + (outputBytes / 4 / 1000) * selectedModel.costPer1kTokensOutput
|
||||
|
||||
logger.info(f"✅ Image generation successful with model: {modelName}")
|
||||
return AiCallResponse(
|
||||
|
|
@ -343,30 +393,30 @@ class AiObjects:
|
|||
)
|
||||
|
||||
# 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."""
|
||||
request = WebSearchRequest(
|
||||
query=query,
|
||||
max_results=max_results,
|
||||
max_results=maxResults,
|
||||
**kwargs
|
||||
)
|
||||
# Get Tavily connector from registry
|
||||
tavily_connector = model_registry.getConnectorForModel("tavily_search")
|
||||
if not tavily_connector:
|
||||
tavilyConnector = model_registry.getConnectorForModel("tavily_search")
|
||||
if not tavilyConnector:
|
||||
raise ValueError("Tavily connector not available")
|
||||
result = await tavily_connector.search(request)
|
||||
result = await tavilyConnector.search(request)
|
||||
|
||||
if result.success and result.documents:
|
||||
return result.documents[0].documentData.results
|
||||
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."""
|
||||
from pydantic import HttpUrl
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# Safely create HttpUrl objects with proper scheme handling
|
||||
http_urls = []
|
||||
httpUrls = []
|
||||
for url in urls:
|
||||
try:
|
||||
# Ensure URL has a scheme
|
||||
|
|
@ -375,44 +425,44 @@ class AiObjects:
|
|||
url = f"https://{url}"
|
||||
|
||||
# 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:
|
||||
logger.warning(f"Skipping invalid URL {url}: {e}")
|
||||
continue
|
||||
|
||||
if not http_urls:
|
||||
if not httpUrls:
|
||||
return []
|
||||
|
||||
request = WebCrawlRequest(
|
||||
urls=http_urls,
|
||||
extract_depth=extract_depth,
|
||||
urls=httpUrls,
|
||||
extract_depth=extractDepth,
|
||||
format=format
|
||||
)
|
||||
# Get Tavily connector from registry
|
||||
tavily_connector = model_registry.getConnectorForModel("tavily_crawl")
|
||||
if not tavily_connector:
|
||||
tavilyConnector = model_registry.getConnectorForModel("tavily_crawl")
|
||||
if not tavilyConnector:
|
||||
raise ValueError("Tavily connector not available")
|
||||
result = await tavily_connector.crawl(request)
|
||||
result = await tavilyConnector.crawl(request)
|
||||
|
||||
if result.success and result.documents:
|
||||
return result.documents[0].documentData.results
|
||||
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."""
|
||||
crawl_results = await self.crawl_websites(urls, extract_depth, format)
|
||||
return {str(result.url): result.content for result in crawl_results}
|
||||
crawlResults = await self.crawlWebsites(urls, extractDepth, format)
|
||||
return {str(result.url): result.content for result in crawlResults}
|
||||
|
||||
# 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)."""
|
||||
logger.debug(f"Reading page: {url}")
|
||||
try:
|
||||
# URL encode the URL to handle spaces and special characters
|
||||
from urllib.parse import quote, urlparse, urlunparse
|
||||
parsed = urlparse(url)
|
||||
encoded_url = urlunparse((
|
||||
encodedUrl = urlunparse((
|
||||
parsed.scheme,
|
||||
parsed.netloc,
|
||||
parsed.path,
|
||||
|
|
@ -423,53 +473,53 @@ class AiObjects:
|
|||
|
||||
# Manually encode query parameters to handle spaces
|
||||
if parsed.query:
|
||||
encoded_query = quote(parsed.query, safe='=&')
|
||||
encoded_url = urlunparse((
|
||||
encodedQuery = quote(parsed.query, safe='=&')
|
||||
encodedUrl = urlunparse((
|
||||
parsed.scheme,
|
||||
parsed.netloc,
|
||||
parsed.path,
|
||||
parsed.params,
|
||||
encoded_query,
|
||||
encodedQuery,
|
||||
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")
|
||||
result = content.get(encoded_url)
|
||||
content = await self.extractContent([encodedUrl], extractDepth, "markdown")
|
||||
result = content.get(encodedUrl)
|
||||
if result:
|
||||
logger.debug(f"Successfully read page {encoded_url}: {len(result)} chars")
|
||||
logger.debug(f"Successfully read page {encodedUrl}: {len(result)} chars")
|
||||
else:
|
||||
logger.warning(f"No content returned for page {encoded_url}")
|
||||
logger.warning(f"No content returned for page {encodedUrl}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read page {url}: {e}")
|
||||
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."""
|
||||
try:
|
||||
content = await self.readPage(url, extract_depth)
|
||||
content = await self.readPage(url, extractDepth)
|
||||
if not content:
|
||||
return []
|
||||
|
||||
links = self._extractLinksFromContent(content, url)
|
||||
# Remove duplicates while preserving order
|
||||
seen = set()
|
||||
unique_links = []
|
||||
uniqueLinks = []
|
||||
for link in links:
|
||||
if link not in seen:
|
||||
seen.add(link)
|
||||
unique_links.append(link)
|
||||
uniqueLinks.append(link)
|
||||
|
||||
logger.debug(f"Extracted {len(unique_links)} unique URLs from {url}")
|
||||
return unique_links
|
||||
logger.debug(f"Extracted {len(uniqueLinks)} unique URLs from {url}")
|
||||
return uniqueLinks
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get URLs from page {url}: {e}")
|
||||
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.)."""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
|
@ -482,35 +532,35 @@ class AiObjects:
|
|||
return not lower.endswith(blocked)
|
||||
|
||||
# Group by domain
|
||||
domain_links = {}
|
||||
domainLinks = {}
|
||||
for link in urls:
|
||||
domain = urlparse(link).netloc
|
||||
if domain not in domain_links:
|
||||
domain_links[domain] = []
|
||||
domain_links[domain].append(link)
|
||||
if domain not in domainLinks:
|
||||
domainLinks[domain] = []
|
||||
domainLinks[domain].append(link)
|
||||
|
||||
# Filter and cap per domain
|
||||
filtered_links = []
|
||||
for domain, domain_link_list in domain_links.items():
|
||||
filteredLinks = []
|
||||
for domain, domainLinkList in domainLinks.items():
|
||||
seen = set()
|
||||
domain_filtered = []
|
||||
domainFiltered = []
|
||||
|
||||
for link in domain_link_list:
|
||||
for link in domainLinkList:
|
||||
if link in seen:
|
||||
continue
|
||||
if not _isHtmlCandidate(link):
|
||||
continue
|
||||
seen.add(link)
|
||||
domain_filtered.append(link)
|
||||
if len(domain_filtered) >= max_per_domain:
|
||||
domainFiltered.append(link)
|
||||
if len(domainFiltered) >= maxPerDomain:
|
||||
break
|
||||
|
||||
filtered_links.extend(domain_filtered)
|
||||
logger.debug(f"Domain {domain}: {len(domain_link_list)} -> {len(domain_filtered)} links")
|
||||
filteredLinks.extend(domainFiltered)
|
||||
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."""
|
||||
try:
|
||||
import re
|
||||
|
|
@ -523,19 +573,19 @@ class AiObjects:
|
|||
|
||||
# If it's a relative URL, make it absolute first
|
||||
if not url.startswith(('http://', 'https://')):
|
||||
url = urljoin(base_url, url)
|
||||
url = urljoin(baseUrl, url)
|
||||
|
||||
# Parse and re-encode the URL properly
|
||||
parsed = urlparse(url)
|
||||
if parsed.query:
|
||||
# Encode query parameters properly
|
||||
encoded_query = quote(parsed.query, safe='=&')
|
||||
encodedQuery = quote(parsed.query, safe='=&')
|
||||
url = urlunparse((
|
||||
parsed.scheme,
|
||||
parsed.netloc,
|
||||
parsed.path,
|
||||
parsed.params,
|
||||
encoded_query,
|
||||
encodedQuery,
|
||||
parsed.fragment
|
||||
))
|
||||
|
||||
|
|
@ -544,45 +594,45 @@ class AiObjects:
|
|||
links = []
|
||||
|
||||
# Extract HTML links: <a href="url"> format
|
||||
html_link_pattern = r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>'
|
||||
html_links = re.findall(html_link_pattern, content, re.IGNORECASE)
|
||||
htmlLinkPattern = r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>'
|
||||
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:'):
|
||||
try:
|
||||
cleaned_url = _cleanUrl(url)
|
||||
links.append(cleaned_url)
|
||||
logger.debug(f"Extracted HTML link: {url} -> {cleaned_url}")
|
||||
cleanedUrl = _cleanUrl(url)
|
||||
links.append(cleanedUrl)
|
||||
logger.debug(f"Extracted HTML link: {url} -> {cleanedUrl}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to clean HTML link {url}: {e}")
|
||||
|
||||
# Extract markdown links: [text](url) format
|
||||
markdown_link_pattern = r'\[([^\]]+)\]\(([^)]+)\)'
|
||||
markdown_links = re.findall(markdown_link_pattern, content)
|
||||
markdownLinkPattern = r'\[([^\]]+)\]\(([^)]+)\)'
|
||||
markdownLinks = re.findall(markdownLinkPattern, content)
|
||||
|
||||
for text, url in markdown_links:
|
||||
for text, url in markdownLinks:
|
||||
if url and not url.startswith('#'):
|
||||
try:
|
||||
cleaned_url = _cleanUrl(url)
|
||||
cleanedUrl = _cleanUrl(url)
|
||||
# Only keep URLs from the same domain
|
||||
if urlparse(cleaned_url).netloc == urlparse(base_url).netloc:
|
||||
links.append(cleaned_url)
|
||||
logger.debug(f"Extracted markdown link: {url} -> {cleaned_url}")
|
||||
if urlparse(cleanedUrl).netloc == urlparse(baseUrl).netloc:
|
||||
links.append(cleanedUrl)
|
||||
logger.debug(f"Extracted markdown link: {url} -> {cleanedUrl}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to clean markdown link {url}: {e}")
|
||||
|
||||
# Extract plain URLs in the text
|
||||
url_pattern = r'https?://[^\s\)]+'
|
||||
plain_urls = re.findall(url_pattern, content)
|
||||
urlPattern = r'https?://[^\s\)]+'
|
||||
plainUrls = re.findall(urlPattern, content)
|
||||
|
||||
for url in plain_urls:
|
||||
for url in plainUrls:
|
||||
try:
|
||||
clean_url = url.rstrip('.,;!?')
|
||||
cleaned_url = _cleanUrl(clean_url)
|
||||
if urlparse(cleaned_url).netloc == urlparse(base_url).netloc:
|
||||
if cleaned_url not in links: # Avoid duplicates
|
||||
links.append(cleaned_url)
|
||||
logger.debug(f"Extracted plain URL: {url} -> {cleaned_url}")
|
||||
cleanUrl = url.rstrip('.,;!?')
|
||||
cleanedUrl = _cleanUrl(cleanUrl)
|
||||
if urlparse(cleanedUrl).netloc == urlparse(baseUrl).netloc:
|
||||
if cleanedUrl not in links: # Avoid duplicates
|
||||
links.append(cleanedUrl)
|
||||
logger.debug(f"Extracted plain URL: {url} -> {cleanedUrl}")
|
||||
except Exception as 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."""
|
||||
|
||||
if options is None:
|
||||
options = AiCallOptions(operationType=OperationType.WEB_RESEARCH)
|
||||
options = AiCallOptions(operationType=OperationTypeEnum.WEB_RESEARCH)
|
||||
|
||||
# Calculate input bytes
|
||||
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")
|
||||
if perplexity_model:
|
||||
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:
|
||||
priceUsd = 0.0
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import logging
|
||||
import re
|
||||
from typing import Dict, Any, List, Optional, Tuple, Union
|
||||
from modules.datamodels.datamodelChat import PromptPlaceholder
|
||||
|
||||
from modules.datamodels.datamodelChat import ChatDocument
|
||||
from modules.datamodels.datamodelChat import PromptPlaceholder, ChatDocument
|
||||
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.datamodelWeb import (
|
||||
WebResearchRequest,
|
||||
|
|
@ -19,6 +16,7 @@ from modules.services.serviceAi.subCoreAi import SubCoreAi
|
|||
from modules.services.serviceAi.subDocumentProcessing import SubDocumentProcessing
|
||||
from modules.services.serviceAi.subWebResearch import SubWebResearch
|
||||
from modules.services.serviceAi.subDocumentGeneration import SubDocumentGeneration
|
||||
from modules.services.serviceAi.subSharedAiUtils import sanitizePromptContent
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -170,68 +168,3 @@ class AiService:
|
|||
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]"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ import json
|
|||
import logging
|
||||
from typing import Dict, Any, List, Optional, Tuple, Union
|
||||
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__)
|
||||
|
||||
|
|
@ -289,23 +295,6 @@ CRITICAL REQUIREMENTS:
|
|||
logger.error(f"Error merging JSON content: {str(e)}")
|
||||
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(
|
||||
self,
|
||||
prompt: str,
|
||||
|
|
@ -359,12 +348,12 @@ CRITICAL REQUIREMENTS:
|
|||
# Build full prompt with placeholders
|
||||
if 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:
|
||||
full_prompt = prompt
|
||||
|
||||
# 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
|
||||
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")
|
||||
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:
|
||||
options = AiCallOptions(operationType=OperationType.IMAGE_ANALYSIS)
|
||||
options = AiCallOptions(operationType=OperationTypeEnum.IMAGE_ANALYSE)
|
||||
else:
|
||||
# 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")
|
||||
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)}")
|
||||
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]:
|
||||
|
|
@ -606,11 +581,11 @@ CRITICAL REQUIREMENTS:
|
|||
|
||||
# Check if model supports the operation type
|
||||
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
|
||||
elif options.operationType == OperationType.IMAGE_GENERATION and "image_generation" not in capabilities:
|
||||
elif options.operationType == OperationTypeEnum.IMAGE_GENERATE and "imageGenerate" not in capabilities:
|
||||
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
|
||||
elif "text_generation" not in capabilities:
|
||||
continue
|
||||
|
|
@ -649,68 +624,4 @@ CRITICAL REQUIREMENTS:
|
|||
"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]"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import time
|
|||
from datetime import datetime, UTC
|
||||
from typing import Dict, Any, List, Optional, Tuple, Union
|
||||
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__)
|
||||
|
||||
|
|
@ -335,9 +335,9 @@ class SubDocumentGeneration:
|
|||
)
|
||||
|
||||
# Prepare the AI call
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
|
||||
requestOptions = AiCallOptions()
|
||||
requestOptions.operationType = OperationType.GENERAL
|
||||
requestOptions.operationType = OperationTypeEnum.GENERAL
|
||||
|
||||
# Create context with the extracted JSON content
|
||||
context = f"Extracted JSON content:\n{json.dumps(docData, indent=2)}"
|
||||
|
|
@ -477,9 +477,9 @@ Consider:
|
|||
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.operationType = OperationType.GENERAL
|
||||
request_options.operationType = OperationTypeEnum.GENERAL
|
||||
|
||||
request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options)
|
||||
response = await ai_service.aiObjects.call(request)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import re
|
|||
import time
|
||||
from typing import Dict, Any, List, Optional, Tuple, Union
|
||||
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.services.serviceExtraction.mainServiceExtraction import ExtractionService
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ class SubDocumentProcessing:
|
|||
"imageQuality": 85,
|
||||
"mergeStrategy": {
|
||||
"useIntelligentMerging": True, # Enable intelligent token-aware merging
|
||||
"modelCapabilities": model_capabilities,
|
||||
"capabilities": model_capabilities,
|
||||
"prompt": prompt,
|
||||
"groupBy": "typeGroup",
|
||||
"orderBy": "id",
|
||||
|
|
@ -145,7 +145,7 @@ class SubDocumentProcessing:
|
|||
"imageQuality": 85,
|
||||
"mergeStrategy": {
|
||||
"useIntelligentMerging": True, # Enable intelligent token-aware merging
|
||||
"modelCapabilities": model_capabilities,
|
||||
"capabilities": model_capabilities,
|
||||
"prompt": prompt,
|
||||
"groupBy": "typeGroup",
|
||||
"orderBy": "id",
|
||||
|
|
@ -240,7 +240,7 @@ class SubDocumentProcessing:
|
|||
"imageQuality": 85,
|
||||
"mergeStrategy": {
|
||||
"useIntelligentMerging": True, # Enable intelligent token-aware merging
|
||||
"modelCapabilities": model_capabilities,
|
||||
"capabilities": model_capabilities,
|
||||
"prompt": custom_prompt, # Use the custom prompt
|
||||
"groupBy": "typeGroup",
|
||||
"orderBy": "id",
|
||||
|
|
@ -666,7 +666,7 @@ CONTINUATION INSTRUCTIONS:
|
|||
elif part.mimeType and part.data and len(part.data.strip()) > 0:
|
||||
# Process any document container as text content
|
||||
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")
|
||||
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
|
||||
request_options = options if options is not None else AiCallOptions()
|
||||
# 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")
|
||||
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
|
||||
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
|
||||
elif options.operationType == OperationType.IMAGE_GENERATION and "image_generation" not in capabilities:
|
||||
elif options.operationType == OperationTypeEnum.IMAGE_GENERATE and "imageGenerate" not in capabilities:
|
||||
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
|
||||
elif "text_generation" not in capabilities:
|
||||
continue
|
||||
|
|
|
|||
164
modules/services/serviceAi/subSharedAiUtils.py
Normal file
164
modules/services/serviceAi/subSharedAiUtils.py
Normal 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"
|
||||
|
|
@ -368,7 +368,7 @@ class SubWebResearch:
|
|||
)
|
||||
|
||||
document = WebResearchActionDocument(
|
||||
documentName=f"web_research_{request.user_prompt[:50]}.json",
|
||||
documentName=f"webResearch_{request.user_prompt[:50]}.json",
|
||||
documentData=documentData,
|
||||
mimeType="application/json"
|
||||
)
|
||||
|
|
@ -376,7 +376,7 @@ class SubWebResearch:
|
|||
return WebResearchActionResult(
|
||||
success=True,
|
||||
documents=[document],
|
||||
resultLabel="web_research_results"
|
||||
resultLabel="webResearch_results"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ def _applyMerging(parts: List[ContentPart], strategy: Dict[str, Any]) -> List[Co
|
|||
|
||||
# Check if intelligent merging is enabled
|
||||
if strategy.get("useIntelligentMerging", False):
|
||||
model_capabilities = strategy.get("modelCapabilities", {})
|
||||
model_capabilities = strategy.get("capabilities", {})
|
||||
subMerger = IntelligentTokenAwareMerger(model_capabilities)
|
||||
|
||||
# Use intelligent merging for all parts
|
||||
|
|
@ -311,19 +311,19 @@ def applyAiIfRequested(extracted: ContentExtracted, options: Dict[str, Any]) ->
|
|||
return extracted
|
||||
|
||||
# Placeholder AI processing based on operationType
|
||||
if operationType == "analyse_content":
|
||||
if operationType == "analyse":
|
||||
# Add analysis metadata to parts
|
||||
for part in extracted.parts:
|
||||
if part.typeGroup in ("text", "table", "structure"):
|
||||
part.metadata["ai_processed"] = True
|
||||
part.metadata["operation_type"] = operationType
|
||||
elif operationType == "generate_plan":
|
||||
elif operationType == "plan":
|
||||
# Add plan generation metadata
|
||||
for part in extracted.parts:
|
||||
if part.typeGroup == "text":
|
||||
part.metadata["ai_processed"] = True
|
||||
part.metadata["operation_type"] = operationType
|
||||
elif operationType == "generate_content":
|
||||
elif operationType == "generate":
|
||||
# Add content generation metadata
|
||||
for part in extracted.parts:
|
||||
part.metadata["ai_processed"] = True
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from datetime import datetime, UTC
|
|||
import base64
|
||||
import io
|
||||
from PIL import Image
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -326,7 +326,7 @@ class BaseRenderer(ABC):
|
|||
try:
|
||||
|
||||
request_options = AiCallOptions()
|
||||
request_options.operationType = OperationType.GENERAL
|
||||
request_options.operationType = OperationTypeEnum.GENERAL
|
||||
|
||||
request = AiCallRequest(prompt=style_template, context="", options=request_options)
|
||||
|
||||
|
|
|
|||
|
|
@ -565,7 +565,7 @@ class RendererDocx(BaseRenderer):
|
|||
|
||||
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."""
|
||||
# Add sections based on prompt structure
|
||||
for section in structure['sections']:
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class RendererImage(BaseRenderer):
|
|||
document_title = extracted_content.get("metadata", {}).get("title", title)
|
||||
|
||||
# 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
|
||||
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)}")
|
||||
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."""
|
||||
try:
|
||||
# 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
|
||||
# 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(
|
||||
prompt=compression_prompt,
|
||||
options=AiCallOptions(
|
||||
operationType=OperationType.GENERAL,
|
||||
operationType=OperationTypeEnum.GENERAL,
|
||||
maxTokens=None, # Let the model use its full context length
|
||||
temperature=0.3 # Lower temperature for more consistent compression
|
||||
)
|
||||
|
|
|
|||
|
|
@ -157,10 +157,10 @@ class RendererPdf(BaseRenderer):
|
|||
return default_styles
|
||||
|
||||
try:
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
|
||||
|
||||
request_options = AiCallOptions()
|
||||
request_options.operationType = OperationType.GENERAL
|
||||
request_options.operationType = OperationTypeEnum.GENERAL
|
||||
|
||||
request = AiCallRequest(prompt=style_template, context="", options=request_options)
|
||||
|
||||
|
|
|
|||
|
|
@ -357,10 +357,10 @@ JSON ONLY. NO OTHER TEXT."""
|
|||
return default_styles
|
||||
|
||||
try:
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
|
||||
|
||||
request_options = AiCallOptions()
|
||||
request_options.operationType = OperationType.GENERAL
|
||||
request_options.operationType = OperationTypeEnum.GENERAL
|
||||
|
||||
request = AiCallRequest(prompt=style_template, context="", options=request_options)
|
||||
|
||||
|
|
|
|||
|
|
@ -274,10 +274,10 @@ class RendererXlsx(BaseRenderer):
|
|||
return default_styles
|
||||
|
||||
try:
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
|
||||
|
||||
request_options = AiCallOptions()
|
||||
request_options.operationType = OperationType.GENERAL
|
||||
request_options.operationType = OperationTypeEnum.GENERAL
|
||||
|
||||
request = AiCallRequest(prompt=style_template, context="", options=request_options)
|
||||
response = await ai_service.aiObjects.call(request)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ This module builds prompts for AI services to extract and generate documents.
|
|||
import json
|
||||
import logging
|
||||
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
|
||||
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.
|
||||
"""
|
||||
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType
|
||||
request_options = AiCallOptions()
|
||||
request_options.operationType = OperationType.GENERAL
|
||||
request_options.operationType = OperationTypeEnum.GENERAL
|
||||
|
||||
request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options)
|
||||
response = await aiService.aiObjects.call(request)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ Please provide a comprehensive summary of this conversation."""
|
|||
documents=None,
|
||||
options={
|
||||
"process_type": "text",
|
||||
"operation_type": "generate_content",
|
||||
"operation_type": "generate",
|
||||
"priority": "speed",
|
||||
"compress_prompt": True,
|
||||
"compress_documents": False,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from datetime import datetime, UTC
|
|||
|
||||
from modules.workflows.methods.methodBase import MethodBase, action
|
||||
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.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.
|
||||
- processingMode (str, optional): basic | advanced | detailed. Default: basic.
|
||||
- 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.
|
||||
- maxCost (float, optional): Cost limit.
|
||||
- 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:
|
||||
# Init progress logger
|
||||
|
|
@ -75,13 +75,55 @@ class MethodAi(MethodBase):
|
|||
if isinstance(documentList, str):
|
||||
documentList = [documentList]
|
||||
resultType = parameters.get("resultType", "txt")
|
||||
processingMode = parameters.get("processingMode", "basic")
|
||||
processingModeStr = parameters.get("processingMode", "basic")
|
||||
includeMetadata = parameters.get("includeMetadata", True)
|
||||
operationType = parameters.get("operationType", "general")
|
||||
priority = parameters.get("priority", "balanced")
|
||||
operationTypeStr = parameters.get("operationType", "general")
|
||||
priorityStr = parameters.get("priority", "balanced")
|
||||
maxCost = parameters.get("maxCost")
|
||||
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:
|
||||
logger.error(f"aiPrompt is missing or empty. Parameters: {parameters}")
|
||||
|
|
@ -113,14 +155,14 @@ class MethodAi(MethodBase):
|
|||
options = AiCallOptions(
|
||||
operationType=operationType,
|
||||
priority=priority,
|
||||
compressPrompt=processingMode != "detailed",
|
||||
compressPrompt=processingMode != ProcessingModeEnum.DETAILED,
|
||||
compressContext=True,
|
||||
processDocumentsIndividually=True,
|
||||
processingMode=processingMode,
|
||||
resultFormat=output_format,
|
||||
maxCost=maxCost,
|
||||
maxProcessingTime=maxProcessingTime,
|
||||
requiredTags=requiredTags
|
||||
capabilities=requiredTags if requiredTags else None
|
||||
)
|
||||
|
||||
# Update progress - calling AI
|
||||
|
|
|
|||
|
|
@ -116,9 +116,9 @@ DELIVERED CONTENT TO CHECK:
|
|||
"""
|
||||
|
||||
# Call AI service for validation
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationType
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
|
||||
request_options = AiCallOptions()
|
||||
request_options.operationType = OperationType.GENERAL
|
||||
request_options.operationType = OperationTypeEnum.GENERAL
|
||||
|
||||
response = await self.services.ai.callAiPlanning(
|
||||
prompt=validationPrompt,
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ CRITICAL: Respond with ONLY the JSON object below. Do not include any explanator
|
|||
"""
|
||||
|
||||
# Call AI service for analysis
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationType
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
|
||||
request_options = AiCallOptions()
|
||||
request_options.operationType = OperationType.GENERAL
|
||||
request_options.operationType = OperationTypeEnum.GENERAL
|
||||
|
||||
response = await self.services.ai.callAiPlanning(
|
||||
prompt=analysisPrompt,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class ProgressTracker:
|
|||
self.partialAchievements = []
|
||||
self.failedAttempts = []
|
||||
self.learningInsights = []
|
||||
self.currentPhase = "planning"
|
||||
self.currentPhase = "plan"
|
||||
|
||||
def updateOperation(self, result: Any, validation: Dict[str, Any], intent: Dict[str, Any]):
|
||||
"""Updates progress tracking based on action result"""
|
||||
|
|
@ -154,4 +154,4 @@ class ProgressTracker:
|
|||
self.partialAchievements = []
|
||||
self.failedAttempts = []
|
||||
self.learningInsights = []
|
||||
self.currentPhase = "planning"
|
||||
self.currentPhase = "plan"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import json
|
|||
import logging
|
||||
from typing import Dict, Any
|
||||
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 (
|
||||
generateTaskPlanningPrompt
|
||||
)
|
||||
|
|
@ -59,7 +59,7 @@ class TaskPlanner:
|
|||
# Create proper context object for task planning using cleaned intent
|
||||
# For task planning, we need to create a minimal TaskStep since TaskContext requires it
|
||||
planningTaskStep = TaskStep(
|
||||
id="planning",
|
||||
id="plan",
|
||||
objective=cleanedObjective,
|
||||
dependencies=[],
|
||||
success_criteria=[],
|
||||
|
|
@ -96,11 +96,11 @@ class TaskPlanner:
|
|||
|
||||
# Centralized AI call: Task planning (quality, detailed) with placeholders
|
||||
options = AiCallOptions(
|
||||
operationType=OperationType.GENERATE_PLAN,
|
||||
priority=Priority.QUALITY,
|
||||
operationType=OperationTypeEnum.PLAN,
|
||||
priority=PriorityEnum.QUALITY,
|
||||
compressPrompt=False,
|
||||
compressContext=False,
|
||||
processingMode=ProcessingMode.DETAILED,
|
||||
processingMode=ProcessingModeEnum.DETAILED,
|
||||
maxCost=0.10,
|
||||
maxProcessingTime=30
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from modules.datamodels.datamodelChat import (
|
|||
ActionResult, ReviewResult, ReviewContext
|
||||
)
|
||||
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.shared.executionState import TaskExecutionState
|
||||
from modules.workflows.processing.shared.promptGenerationActionsActionplan import (
|
||||
|
|
@ -125,11 +125,11 @@ class ActionplanMode(BaseMode):
|
|||
|
||||
# Centralized AI call: Action planning (quality, detailed) with placeholders
|
||||
options = AiCallOptions(
|
||||
operationType=OperationType.GENERATE_PLAN,
|
||||
priority=Priority.QUALITY,
|
||||
operationType=OperationTypeEnum.PLAN,
|
||||
priority=PriorityEnum.QUALITY,
|
||||
compressPrompt=False,
|
||||
compressContext=False,
|
||||
processingMode=ProcessingMode.DETAILED,
|
||||
processingMode=ProcessingModeEnum.DETAILED,
|
||||
maxCost=0.10,
|
||||
maxProcessingTime=30
|
||||
)
|
||||
|
|
@ -457,11 +457,11 @@ class ActionplanMode(BaseMode):
|
|||
|
||||
# Centralized AI call: Result validation (balanced analysis) with placeholders
|
||||
options = AiCallOptions(
|
||||
operationType=OperationType.ANALYSE_CONTENT,
|
||||
priority=Priority.BALANCED,
|
||||
operationType=OperationTypeEnum.ANALYSE,
|
||||
priority=PriorityEnum.BALANCED,
|
||||
compressPrompt=True,
|
||||
compressContext=False,
|
||||
processingMode=ProcessingMode.ADVANCED,
|
||||
processingMode=ProcessingModeEnum.ADVANCED,
|
||||
maxCost=0.05,
|
||||
maxProcessingTime=30
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from modules.datamodels.datamodelChat import (
|
|||
ActionResult
|
||||
)
|
||||
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.shared.executionState import TaskExecutionState, shouldContinue
|
||||
from modules.workflows.processing.shared.promptGenerationActionsReact import (
|
||||
|
|
@ -187,11 +187,11 @@ class ReactMode(BaseMode):
|
|||
|
||||
# Centralized AI call for plan selection (use plan generation quality)
|
||||
options = AiCallOptions(
|
||||
operationType=OperationType.GENERATE_PLAN,
|
||||
priority=Priority.QUALITY,
|
||||
operationType=OperationTypeEnum.PLAN,
|
||||
priority=PriorityEnum.QUALITY,
|
||||
compressPrompt=False,
|
||||
compressContext=False,
|
||||
processingMode=ProcessingMode.DETAILED,
|
||||
processingMode=ProcessingModeEnum.DETAILED,
|
||||
maxCost=0.10,
|
||||
maxProcessingTime=30
|
||||
)
|
||||
|
|
@ -296,11 +296,11 @@ class ReactMode(BaseMode):
|
|||
|
||||
# Centralized AI call for parameter suggestion (balanced analysis)
|
||||
options = AiCallOptions(
|
||||
operationType=OperationType.ANALYSE_CONTENT,
|
||||
priority=Priority.BALANCED,
|
||||
operationType=OperationTypeEnum.ANALYSE,
|
||||
priority=PriorityEnum.BALANCED,
|
||||
compressPrompt=True,
|
||||
compressContext=False,
|
||||
processingMode=ProcessingMode.ADVANCED,
|
||||
processingMode=ProcessingModeEnum.ADVANCED,
|
||||
maxCost=0.05,
|
||||
maxProcessingTime=30,
|
||||
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)
|
||||
options = AiCallOptions(
|
||||
operationType=OperationType.ANALYSE_CONTENT,
|
||||
priority=Priority.BALANCED,
|
||||
operationType=OperationTypeEnum.ANALYSE,
|
||||
priority=PriorityEnum.BALANCED,
|
||||
compressPrompt=True,
|
||||
compressContext=False,
|
||||
processingMode=ProcessingMode.ADVANCED,
|
||||
processingMode=ProcessingModeEnum.ADVANCED,
|
||||
maxCost=0.05,
|
||||
maxProcessingTime=30
|
||||
)
|
||||
|
|
@ -718,8 +718,8 @@ Return only the user-friendly message, no technical details."""
|
|||
prompt=prompt,
|
||||
placeholders=None,
|
||||
options=AiCallOptions(
|
||||
operationType=OperationType.GENERATE_CONTENT,
|
||||
priority=Priority.SPEED,
|
||||
operationType=OperationTypeEnum.GENERATE,
|
||||
priority=PriorityEnum.SPEED,
|
||||
compressPrompt=True,
|
||||
maxCost=0.01,
|
||||
maxProcessingTime=5
|
||||
|
|
@ -759,8 +759,8 @@ Return only the user-friendly message, no technical details."""
|
|||
prompt=prompt,
|
||||
placeholders=None,
|
||||
options=AiCallOptions(
|
||||
operationType=OperationType.GENERATE_CONTENT,
|
||||
priority=Priority.SPEED,
|
||||
operationType=OperationTypeEnum.GENERATE,
|
||||
priority=PriorityEnum.SPEED,
|
||||
compressPrompt=True,
|
||||
maxCost=0.01,
|
||||
maxProcessingTime=5
|
||||
|
|
|
|||
Loading…
Reference in a new issue