diff --git a/modules/aicore/aicorePluginAnthropic.py b/modules/aicore/aicorePluginAnthropic.py index 6091f872..b5d552cd 100644 --- a/modules/aicore/aicorePluginAnthropic.py +++ b/modules/aicore/aicorePluginAnthropic.py @@ -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, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum +from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse # Configure logger logger = logging.getLogger(__name__) @@ -88,28 +88,26 @@ class AiAnthropic(BaseConnectorAi): ] - async def callAiBasic(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> Dict[str, Any]: + async def callAiBasic(self, modelCall: AiModelCall) -> AiModelResponse: """ - Calls the Anthropic API with the given messages. + Calls the Anthropic API with the given messages using standardized pattern. Args: - messages: List of messages in OpenAI format (role, content) - temperature: Temperature for response generation (0.0-1.0) - maxTokens: Maximum number of tokens in the response + modelCall: AiModelCall with messages and options Returns: - The response in OpenAI format + AiModelResponse with content and metadata Raises: HTTPException: For errors in API communication """ try: - # Use parameters from configuration if none were overridden - if temperature is None: - temperature = self.config.get("temperature", 0.2) - - # Don't set maxTokens from config - let the model use its full context length - # Our continuation system handles stopping early via prompt engineering + # Extract parameters from modelCall + messages = modelCall.messages + model = modelCall.model + options = modelCall.options + temperature = options.get("temperature", self.config.get("temperature", 0.2)) + maxTokens = model.maxTokens # Transform OpenAI-style messages to Anthropic format: # - Move any 'system' role content to top-level 'system' @@ -205,23 +203,13 @@ class AiAnthropic(BaseConnectorAi): logger.warning(f"Anthropic API returned empty content. Full response: {anthropicResponse}") content = "[Anthropic API returned empty response]" - # Return in OpenAI format - return { - "id": anthropicResponse.get("id", ""), - "object": "chat.completion", - "created": anthropicResponse.get("created", 0), - "model": anthropicResponse.get("model", self.modelName), - "choices": [ - { - "message": { - "role": "assistant", - "content": content - }, - "index": 0, - "finish_reason": "stop" - } - ] - } + # Return standardized response + return AiModelResponse( + content=content, + success=True, + modelId=self.modelName, + metadata={"response_id": anthropicResponse.get("id", "")} + ) except Exception as e: logger.error(f"Error calling Anthropic API: {str(e)}") diff --git a/modules/aicore/aicorePluginInternal.py b/modules/aicore/aicorePluginInternal.py index e0473678..b121f595 100644 --- a/modules/aicore/aicorePluginInternal.py +++ b/modules/aicore/aicorePluginInternal.py @@ -1,7 +1,7 @@ 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 +from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse # Configure logger logger = logging.getLogger(__name__) @@ -76,158 +76,36 @@ class AiInternal(BaseConnectorAi): ) ] - async def extractDocument(self, documentData: Union[str, bytes], extractionType: str = "basic") -> Dict[str, Any]: + async def extractDocument(self, modelCall: AiModelCall) -> AiModelResponse: """ - 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 + NOP - we only need the model for price calculations """ - 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 - } + logger.error(f"Document extraction not to call here") + return AiModelResponse( + content="", + success=False, + error="Internal connector should not be called directly" + ) - async def generateDocument(self, template: str, data: Dict[str, Any], format: str = "html") -> Dict[str, Any]: + async def generateDocument(self, modelCall: AiModelCall) -> AiModelResponse: """ - 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 + NOP - we only need the model for price calculations """ - 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 - } + logger.error(f"Document generation not to call here") + return AiModelResponse( + content="", + success=False, + error="Internal connector should not be called directly" + ) - async def renderDocument(self, content: str, targetFormat: str, options: Dict[str, Any] = None) -> Dict[str, Any]: + async def renderDocument(self, modelCall: AiModelCall) -> AiModelResponse: """ - 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 + NOP - we only need the model for price calculations """ - 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"
{content}" - 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 - } + logger.error(f"Document rendering not to call here") + return AiModelResponse( + content="", + success=False, + error="Internal connector should not be called directly" + ) - 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 diff --git a/modules/aicore/aicorePluginOpenai.py b/modules/aicore/aicorePluginOpenai.py index 1202d004..849a4600 100644 --- a/modules/aicore/aicorePluginOpenai.py +++ b/modules/aicore/aicorePluginOpenai.py @@ -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, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum +from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse # Configure logger logger = logging.getLogger(__name__) @@ -125,40 +125,34 @@ class AiOpenai(BaseConnectorAi): ) ] - async def callAiBasic(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> str: + async def callAiBasic(self, modelCall: AiModelCall) -> AiModelResponse: """ - Calls the OpenAI API with the given messages. + Calls the OpenAI API with the given messages using standardized pattern. Args: - messages: List of messages in OpenAI format (role, content) - temperature: Temperature for response generation (0.0-1.0) - maxTokens: Maximum number of tokens in the response + modelCall: AiModelCall with messages and options Returns: - The response from the OpenAI API + AiModelResponse with content and metadata Raises: HTTPException: For errors in API communication """ try: - # Use parameters from configuration if none were overridden - if temperature is None: - temperature = self.config.get("temperature", 0.2) - - # Don't set maxTokens from config - let the model use its full context length - # Our continuation system handles stopping early via prompt engineering + # Extract parameters from modelCall + messages = modelCall.messages + model = modelCall.model + options = modelCall.options + temperature = options.get("temperature", self.config.get("temperature", 0.2)) + maxTokens = model.maxTokens payload = { "model": self.modelName, "messages": messages, - "temperature": temperature + "temperature": temperature, + "max_tokens": maxTokens } - # Add max_tokens - use provided value or throw error - if maxTokens is None: - raise ValueError("maxTokens must be provided for OpenAI API calls") - payload["max_tokens"] = maxTokens - response = await self.httpClient.post( self.apiUrl, json=payload @@ -186,7 +180,13 @@ class AiOpenai(BaseConnectorAi): responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] - return content + + return AiModelResponse( + content=content, + success=True, + modelId=self.modelName, + metadata={"response_id": responseJson.get("id", "")} + ) except ContextLengthExceededException: # Re-raise context length exceptions without wrapping diff --git a/modules/aicore/aicorePluginPerplexity.py b/modules/aicore/aicorePluginPerplexity.py index f2f80f3d..8ce4e9da 100644 --- a/modules/aicore/aicorePluginPerplexity.py +++ b/modules/aicore/aicorePluginPerplexity.py @@ -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, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum +from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse # Configure logger logger = logging.getLogger(__name__) @@ -141,40 +141,34 @@ class AiPerplexity(BaseConnectorAi): ) ] - async def callAiBasic(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> str: + async def callAiBasic(self, modelCall: AiModelCall) -> AiModelResponse: """ - Calls the Perplexity API with the given messages. + Calls the Perplexity API with the given messages using standardized pattern. Args: - messages: List of messages in OpenAI format (role, content) - temperature: Temperature for response generation (0.0-1.0) - maxTokens: Maximum number of tokens in the response + modelCall: AiModelCall with messages and options Returns: - The response from the Perplexity API + AiModelResponse with content and metadata Raises: HTTPException: For errors in API communication """ try: - # Use parameters from configuration if none were overridden - if temperature is None: - temperature = self.config.get("temperature", 0.2) - - # Don't set maxTokens from config - let the model use its full context length - # Our continuation system handles stopping early via prompt engineering + # Extract parameters from modelCall + messages = modelCall.messages + model = modelCall.model + options = modelCall.options + temperature = options.get("temperature", self.config.get("temperature", 0.2)) + maxTokens = model.maxTokens payload = { "model": self.modelName, "messages": messages, - "temperature": temperature + "temperature": temperature, + "max_tokens": maxTokens } - # Add max_tokens - use provided value or throw error - if maxTokens is None: - raise ValueError("maxTokens must be provided for Perplexity API calls") - payload["max_tokens"] = maxTokens - response = await self.httpClient.post( self.apiUrl, json=payload @@ -198,7 +192,13 @@ class AiPerplexity(BaseConnectorAi): responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] - return content + + return AiModelResponse( + content=content, + success=True, + modelId=self.modelName, + metadata={"response_id": responseJson.get("id", "")} + ) except Exception as e: logger.error(f"Error calling Perplexity API: {str(e)}") diff --git a/modules/aicore/aicorePluginTavily.py b/modules/aicore/aicorePluginTavily.py index 73966dca..b6d25f74 100644 --- a/modules/aicore/aicorePluginTavily.py +++ b/modules/aicore/aicorePluginTavily.py @@ -9,21 +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, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum -from modules.datamodels.datamodelWeb import ( - WebSearchActionResult, - WebSearchActionDocument, - WebSearchDocumentData, - WebSearchResultItem, - WebCrawlActionResult, - WebCrawlActionDocument, - WebCrawlDocumentData, - WebCrawlResultItem, - WebScrapeActionResult, - WebScrapeActionDocument, - WebSearchDocumentData as WebScrapeDocumentData, - WebScrapeResultItem, -) +from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelResponse logger = logging.getLogger(__name__) @@ -39,6 +25,32 @@ class WebCrawlResult: url: str content: str +@dataclass +class WebResearchRequest: + """Ultra-simplified web research request""" + user_prompt: str + urls: Optional[List[str]] = None + max_results: int = 5 + max_pages: int = 10 + search_depth: str = "basic" + extract_depth: str = "advanced" + format: str = "markdown" + country: Optional[str] = None + time_range: Optional[str] = None + topic: Optional[str] = None + language: Optional[str] = None + +@dataclass +class WebResearchResult: + """Ultra-simplified web research result - just success/error + documents""" + success: bool = True + error: Optional[str] = None + documents: List[dict] = None # Simple dict instead of ActionDocument + + def __post_init__(self): + if self.documents is None: + self.documents = [] + class ConnectorWeb(BaseConnectorAi): """Tavily web search connector.""" @@ -152,127 +164,167 @@ class ConnectorWeb(BaseConnectorAi): webSearchMaxResults=int(APP_CONFIG.get("Web_Search_MAX_RESULTS", "20")), ) - # Standardized methods returning ActionResults for the interface to consume - async def search(self, request) -> "WebSearchActionResult": + # Standardized method using AiModelCall/AiModelResponse pattern + + async def search(self, modelCall) -> "AiModelResponse": + """Search using standardized AiModelCall/AiModelResponse pattern""" try: + # Extract parameters from modelCall + query = modelCall.messages[0]["content"] if modelCall.messages else "" + options = modelCall.options + raw_results = await self._search( - query=request.query, - max_results=request.max_results, - search_depth=request.search_depth, - time_range=request.time_range, - topic=request.topic, - include_domains=request.include_domains, - exclude_domains=request.exclude_domains, - language=request.language, - include_answer=request.include_answer, - include_raw_content=request.include_raw_content, + query=query, + max_results=options.get("max_results", 5), + search_depth=options.get("search_depth"), + time_range=options.get("time_range"), + topic=options.get("topic"), + include_domains=options.get("include_domains"), + exclude_domains=options.get("exclude_domains"), + language=options.get("language"), + include_answer=options.get("include_answer"), + include_raw_content=options.get("include_raw_content"), ) + + # Convert to JSON string + results_json = { + "query": query, + "results": [ + { + "title": result.title, + "url": result.url, + "content": getattr(result, 'raw_content', None) + } + for result in raw_results + ], + "total_count": len(raw_results) + } + + import json + content = json.dumps(results_json, indent=2) + + return AiModelResponse( + content=content, + success=True, + metadata={ + "total_count": len(raw_results), + "search_depth": options.get("search_depth", "basic") + } + ) + except Exception as e: - return WebSearchActionResult(success=False, error=str(e)) - - result_items = [ - WebSearchResultItem( - title=result.title, - url=result.url, - raw_content=getattr(result, 'raw_content', None) + return AiModelResponse( + content="", + success=False, + error=str(e) ) - for result in raw_results - ] - document_data = WebSearchDocumentData( - query=request.query, - results=result_items, - total_count=len(result_items), - ) - - document = WebSearchActionDocument( - documentName=f"web_search_results_{get_utc_timestamp()}.json", - documentData=document_data, - mimeType="application/json", - ) - - return WebSearchActionResult( - success=True, documents=[document], resultLabel="web_search_results" - ) - - async def crawl(self, request) -> "WebCrawlActionResult": + async def crawl(self, modelCall) -> "AiModelResponse": + """Crawl using standardized AiModelCall/AiModelResponse pattern""" try: + # Extract parameters from modelCall + options = modelCall.options + urls = options.get("urls", []) + raw_results = await self._crawl( - [str(u) for u in request.urls], - extract_depth=request.extract_depth, - format=request.format, + urls, + extract_depth=options.get("extract_depth"), + format=options.get("format"), ) + + # Convert to JSON string + results_json = { + "urls": urls, + "results": [ + { + "url": result.url, + "content": result.content + } + for result in raw_results + ], + "total_count": len(raw_results) + } + + import json + content = json.dumps(results_json, indent=2) + + return AiModelResponse( + content=content, + success=True, + metadata={ + "total_count": len(raw_results), + "extract_depth": options.get("extract_depth", "basic") + } + ) + except Exception as e: - return WebCrawlActionResult(success=False, error=str(e)) + return AiModelResponse( + content="", + success=False, + error=str(e) + ) - result_items = [ - WebCrawlResultItem(url=result.url, content=result.content) - for result in raw_results - ] - - document_data = WebCrawlDocumentData( - urls=[str(u) for u in request.urls], - results=result_items, - total_count=len(result_items), - ) - - document = WebCrawlActionDocument( - documentName=f"web_crawl_results_{get_utc_timestamp()}.json", - documentData=document_data, - mimeType="application/json", - ) - - return WebCrawlActionResult( - success=True, documents=[document], resultLabel="web_crawl_results" - ) - - async def scrape(self, request) -> "WebScrapeActionResult": + async def scrape(self, modelCall) -> "AiModelResponse": + """Scrape using standardized AiModelCall/AiModelResponse pattern""" try: + # Extract parameters from modelCall + query = modelCall.messages[0]["content"] if modelCall.messages else "" + options = modelCall.options + search_results = await self._search( - query=request.query, - max_results=request.max_results, - search_depth=request.search_depth, - time_range=request.time_range, - topic=request.topic, - include_domains=request.include_domains, - exclude_domains=request.exclude_domains, - language=request.language, - include_answer=request.include_answer, - include_raw_content=request.include_raw_content, + query=query, + max_results=options.get("max_results", 5), + search_depth=options.get("search_depth"), + time_range=options.get("time_range"), + topic=options.get("topic"), + include_domains=options.get("include_domains"), + exclude_domains=options.get("exclude_domains"), + language=options.get("language"), + include_answer=options.get("include_answer"), + include_raw_content=options.get("include_raw_content"), ) - except Exception as e: - return WebScrapeActionResult(success=False, error=str(e)) - try: urls = [result.url for result in search_results] crawl_results = await self._crawl( urls, - extract_depth=request.extract_depth, - format=request.format, + extract_depth=options.get("extract_depth"), + format=options.get("format"), ) + + # Convert to JSON string + results_json = { + "query": query, + "results": [ + { + "url": result.url, + "content": result.content + } + for result in crawl_results + ], + "total_count": len(crawl_results) + } + + import json + content = json.dumps(results_json, indent=2) + + return AiModelResponse( + content=content, + success=True, + metadata={ + "total_count": len(crawl_results), + "search_depth": options.get("search_depth", "basic"), + "extract_depth": options.get("extract_depth", "basic") + } + ) + except Exception as e: - return WebScrapeActionResult(success=False, error=str(e)) + return AiModelResponse( + content="", + success=False, + error=str(e) + ) - result_items = [ - WebScrapeResultItem(url=result.url, content=result.content) - for result in crawl_results - ] - - document_data = WebScrapeDocumentData( - query=request.query, - results=result_items, - total_count=len(result_items), - ) - - document = WebScrapeActionDocument( - documentName=f"web_scrape_results_{get_utc_timestamp()}.json", - documentData=document_data, - mimeType="application/json", - ) - - return WebScrapeActionResult( - success=True, documents=[document], resultLabel="web_scrape_results" - ) + # Helper Functions async def _search_urls_raw(self, *, diff --git a/modules/datamodels/datamodelAi.py b/modules/datamodels/datamodelAi.py index f154a79c..da5c1228 100644 --- a/modules/datamodels/datamodelAi.py +++ b/modules/datamodels/datamodelAi.py @@ -185,3 +185,31 @@ class EnhancedAiCallOptions(AiCallOptions): description="Separator between chunks in merged output" ) + +class AiModelCall(BaseModel): + """Standardized input for AI model calls.""" + + messages: List[Dict[str, Any]] = Field(description="Messages in OpenAI format (role, content)") + model: Optional[AiModel] = Field(default=None, description="The AI model being called") + options: Dict[str, Any] = Field(default_factory=dict, description="Additional model-specific options") + + class Config: + arbitraryTypesAllowed = True + + +class AiModelResponse(BaseModel): + """Standardized output from AI model calls.""" + + content: str = Field(description="The AI response content") + success: bool = Field(default=True, description="Whether the call was successful") + error: Optional[str] = Field(default=None, description="Error message if success=False") + + # Optional metadata that models can include + modelId: Optional[str] = Field(default=None, description="Model identifier used") + processingTime: Optional[float] = Field(default=None, description="Processing time in seconds") + tokensUsed: Optional[Dict[str, int]] = Field(default=None, description="Token usage (input, output, total)") + metadata: Optional[Dict[str, Any]] = Field(default=None, description="Additional model-specific metadata") + + class Config: + arbitraryTypesAllowed = True + diff --git a/modules/datamodels/datamodelDocument.py b/modules/datamodels/datamodelDocument.py index 4c37c106..a437b6f1 100644 --- a/modules/datamodels/datamodelDocument.py +++ b/modules/datamodels/datamodelDocument.py @@ -121,10 +121,5 @@ class JsonMergeResult(BaseModel): metadata: Dict[str, Any] = Field(default_factory=dict, description="Merge process metadata") -# Update forward references (compatible with Pydantic v1 and v2) -try: - # Pydantic v2 - ListItem.model_rebuild() -except AttributeError: - # Pydantic v1 - ListItem.update_forward_refs() +# Update forward references +ListItem.model_rebuild() diff --git a/modules/datamodels/datamodelWeb.py b/modules/datamodels/datamodelWeb.py deleted file mode 100644 index bc1e03e3..00000000 --- a/modules/datamodels/datamodelWeb.py +++ /dev/null @@ -1,142 +0,0 @@ -"""Web-related modules""" -from pydantic import BaseModel, Field, HttpUrl -from typing import List, Optional, Literal, Dict, Any -from modules.shared.configuration import APP_CONFIG -from modules.datamodels.datamodelChat import ActionDocument, ActionResult - - -WEB_SEARCH_MAX_QUERY_LENGTH: int = int(APP_CONFIG.get("Web_Search_MAX_QUERY_LENGTH", "400")) -WEB_SEARCH_MAX_RESULTS: int = int(APP_CONFIG.get("Web_Search_MAX_RESULTS", "20")) -WEB_SEARCH_MIN_RESULTS: int = int(APP_CONFIG.get("Web_Search_MIN_RESULTS", "1")) - - -class WebResearchOptions(BaseModel): - """Advanced options for web research workflow""" - max_pages: int = Field(default=10, ge=1, le=50, description="Maximum pages to crawl") - search_depth: Literal["basic", "advanced"] = Field(default="basic", description="Tavily search depth") - extract_depth: Literal["basic", "advanced"] = Field(default="advanced", description="Tavily extract depth") - format: Literal["text", "markdown"] = Field(default="markdown", description="Content format") - return_report: bool = Field(default=True, description="Return formatted report or raw data") - pages_search_depth: int = Field(default=1, ge=1, le=5, description="How deep to crawl: 1=main pages only, 2=main+sub-pages, 3=main+sub+sub-sub, etc.") - country: Optional[str] = Field(default=None, description="Country code for search bias") - time_range: Optional[Literal["d", "w", "m", "y"]] = Field(default=None, description="Time range for search") - topic: Optional[Literal["general", "news", "academic"]] = Field(default=None, description="Search topic") - language: Optional[str] = Field(default=None, description="Language code") - include_answer: Optional[bool] = Field(default=None, description="Include AI answer") - include_raw_content: Optional[bool] = Field(default=None, description="Include raw content") - -class WebResearchRequest(BaseModel): - """Main web research request""" - user_prompt: str = Field(min_length=1, max_length=WEB_SEARCH_MAX_QUERY_LENGTH, description="User's research question or prompt") - urls: Optional[List[str]] = Field(default=None, description="Specific URLs to crawl (optional)") - max_results: int = Field(default=5, ge=1, le=WEB_SEARCH_MAX_RESULTS, description="Max search results") - options: WebResearchOptions = Field(default_factory=WebResearchOptions, description="Advanced options") - -class WebSearchResultItem(BaseModel): - """Individual search result""" - title: str - url: HttpUrl - raw_content: Optional[str] = Field(default=None, description="Raw HTML content") - -class WebCrawlResultItem(BaseModel): - """Individual crawl result""" - url: HttpUrl - content: str - -class WebResearchDocumentData(BaseModel): - """Complete web research results""" - user_prompt: str - websites_analyzed: int - additional_links_found: int - analysis_result: str - sources: List[WebSearchResultItem] - additional_links: List[str] - individual_content: Optional[Dict[str, str]] = None # URL -> content mapping - debug_info: Optional[Dict[str, Any]] = None - -class WebResearchActionDocument(ActionDocument): - documentData: WebResearchDocumentData - -class WebResearchActionResult(ActionResult): - documents: List[WebResearchActionDocument] = Field(default_factory=list) - -# Legacy models for connector compatibility - -class WebSearchDocumentData(BaseModel): - """Search results document data""" - query: str - results: List[WebSearchResultItem] - total_count: int - -class WebSearchActionDocument(ActionDocument): - documentData: WebSearchDocumentData - -class WebSearchActionResult(ActionResult): - documents: List[WebSearchActionDocument] = Field(default_factory=list) - -class WebCrawlDocumentData(BaseModel): - """Crawl results document data""" - urls: List[HttpUrl] - results: List[WebCrawlResultItem] - total_count: int - -class WebCrawlActionDocument(ActionDocument): - documentData: WebCrawlDocumentData - -class WebCrawlActionResult(ActionResult): - documents: List[WebCrawlActionDocument] = Field(default_factory=list) - -class WebScrapeDocumentData(BaseModel): - """Scrape results document data""" - query: str - results: List[WebSearchResultItem] - total_count: int - -class WebScrapeActionDocument(ActionDocument): - documentData: WebScrapeDocumentData - -class WebScrapeActionResult(ActionResult): - documents: List[WebScrapeActionDocument] = Field(default_factory=list) - -class WebSearchRequest(BaseModel): - """Search request for Tavily""" - query: str - max_results: int = 5 - search_depth: Optional[Literal["basic", "advanced"]] = None - time_range: Optional[Literal["d", "w", "m", "y"]] = None - topic: Optional[Literal["general", "news", "academic"]] = None - include_domains: Optional[List[str]] = None - exclude_domains: Optional[List[str]] = None - language: Optional[str] = None - include_answer: Optional[bool] = None - include_raw_content: Optional[bool] = None - auto_parameters: Optional[bool] = None - country: Optional[str] = None - -class WebCrawlRequest(BaseModel): - """Crawl request for Tavily""" - urls: List[HttpUrl] - extract_depth: Optional[Literal["basic", "advanced"]] = None - format: Optional[Literal["text", "markdown"]] = None - -class WebScrapeRequest(BaseModel): - """Scrape request for Tavily""" - query: str - max_results: int = 5 - search_depth: Optional[Literal["basic", "advanced"]] = None - time_range: Optional[Literal["d", "w", "m", "y"]] = None - topic: Optional[Literal["general", "news", "academic"]] = None - include_domains: Optional[List[str]] = None - exclude_domains: Optional[List[str]] = None - language: Optional[str] = None - include_answer: Optional[bool] = None - include_raw_content: Optional[bool] = None - auto_parameters: Optional[bool] = None - country: Optional[str] = None - extract_depth: Optional[Literal["basic", "advanced"]] = None - format: Optional[Literal["text", "markdown"]] = None - -class WebScrapeResultItem(BaseModel): - """Individual scrape result""" - url: HttpUrl - content: str diff --git a/modules/interfaces/interfaceAiObjects.py b/modules/interfaces/interfaceAiObjects.py index 918944a7..bc082ed5 100644 --- a/modules/interfaces/interfaceAiObjects.py +++ b/modules/interfaces/interfaceAiObjects.py @@ -13,17 +13,10 @@ from modules.datamodels.datamodelAi import ( AiCallOptions, AiCallRequest, AiCallResponse, - OperationTypeEnum, + OperationTypeEnum, + AiModelCall, + AiModelResponse, ) -from modules.datamodels.datamodelWeb import ( - WebResearchRequest, - WebResearchActionResult, - WebSearchResultItem, - WebCrawlResultItem, - WebSearchRequest, - WebCrawlRequest, -) -from modules.datamodels.datamodelChat import ActionDocument # Dynamic model registry - models are now loaded from connectors via aicore system @@ -94,8 +87,7 @@ class AiObjects: context = request.context or "" options = request.options - # Calculate input bytes - inputBytes = len((prompt + context).encode("utf-8")) + # Input bytes will be calculated inside _callWithModel # Compress optionally (prompt/context) - simple truncation fallback kept here def _maybeTruncate(text: str, limit: int) -> str: @@ -109,11 +101,7 @@ class AiObjects: if options.compressContext and len(context.encode("utf-8")) > 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) + # Generation parameters are handled inside _callWithModel # Get failover models for this operation type availableModels = modelRegistry.getAvailableModels() @@ -127,7 +115,7 @@ class AiObjects: modelName="error", priceUsd=0.0, processingTime=0.0, - bytesSent=inputBytes, + bytesSent=0, bytesReceived=0, errorCount=1 ) @@ -139,7 +127,7 @@ class AiObjects: logger.info(f"Attempting AI call with model: {model.name} (attempt {attempt + 1}/{len(failoverModelList)})") # Call the model - response = await self._callWithModel(model, prompt, context, temperature, maxTokens, inputBytes) + response = await self._callWithModel(model, prompt, context) logger.info(f"✅ AI call successful with model: {model.name}") return response @@ -165,7 +153,7 @@ class AiObjects: modelName="error", priceUsd=0.0, processingTime=0.0, - bytesSent=inputBytes, + bytesSent=0, bytesReceived=0, errorCount=1 ) @@ -216,7 +204,7 @@ class AiObjects: if partSize <= modelContextBytes: # Part fits - call AI directly - response = await self._callWithModel(model, prompt, contentPart.data, 0.2, None, partSize) + response = await self._callWithModel(model, prompt, contentPart.data) logger.info(f"✅ Content part processed successfully with model: {model.name}") return response else: @@ -228,7 +216,7 @@ class AiObjects: # Process each chunk chunkResults = [] for chunk in chunks: - chunkResponse = await self._callWithModel(model, prompt, chunk['data'], 0.2, None, chunk['size']) + chunkResponse = await self._callWithModel(model, prompt, chunk['data']) chunkResults.append(chunkResponse) # Merge chunk results @@ -405,8 +393,11 @@ class AiObjects: errorCount=1 ) - async def _callWithModel(self, model: AiModel, prompt: str, context: str, temperature: float, maxTokens: int, inputBytes: int) -> AiCallResponse: + async def _callWithModel(self, model: AiModel, prompt: str, context: str) -> AiCallResponse: """Call a specific model and return the response.""" + # Calculate input bytes from prompt and context + inputBytes = len((prompt + context).encode('utf-8')) + # Replace