tuning and aligning ai models

This commit is contained in:
ValueOn AG 2025-10-25 22:27:27 +02:00
parent 8d25ed6fc3
commit e8c3052176
9 changed files with 711 additions and 155 deletions

View file

@ -112,7 +112,9 @@ class AiAnthropic(BaseConnectorAi):
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
# Transform OpenAI-style messages to Anthropic format: # Transform OpenAI-style messages to Anthropic format:
@ -237,8 +239,8 @@ class AiAnthropic(BaseConnectorAi):
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
prompt = messages[0]["content"] if messages else "" prompt = messages[0]["content"] if messages else ""
imageData = options.get("imageData") imageData = getattr(options, "imageData", None)
mimeType = options.get("mimeType") mimeType = getattr(options, "mimeType", None)
# Debug logging # Debug logging
logger.info(f"callAiImage called with imageData type: {type(imageData)}, length: {len(imageData) if imageData else 0}, mimeType: {mimeType}") logger.info(f"callAiImage called with imageData type: {type(imageData)}, length: {len(imageData) if imageData else 0}, mimeType: {mimeType}")

View file

@ -51,7 +51,7 @@ class AiOpenai(BaseConnectorAi):
connectorType="openai", connectorType="openai",
apiUrl="https://api.openai.com/v1/chat/completions", apiUrl="https://api.openai.com/v1/chat/completions",
temperature=0.2, temperature=0.2,
maxTokens=128000, maxTokens=16384,
contextLength=128000, contextLength=128000,
costPer1kTokensInput=0.03, costPer1kTokensInput=0.03,
costPer1kTokensOutput=0.06, costPer1kTokensOutput=0.06,
@ -76,7 +76,7 @@ class AiOpenai(BaseConnectorAi):
connectorType="openai", connectorType="openai",
apiUrl="https://api.openai.com/v1/chat/completions", apiUrl="https://api.openai.com/v1/chat/completions",
temperature=0.2, temperature=0.2,
maxTokens=16000, maxTokens=4096,
contextLength=16000, contextLength=16000,
costPer1kTokensInput=0.0015, costPer1kTokensInput=0.0015,
costPer1kTokensOutput=0.002, costPer1kTokensOutput=0.002,
@ -100,7 +100,7 @@ class AiOpenai(BaseConnectorAi):
connectorType="openai", connectorType="openai",
apiUrl="https://api.openai.com/v1/chat/completions", apiUrl="https://api.openai.com/v1/chat/completions",
temperature=0.2, temperature=0.2,
maxTokens=128000, maxTokens=16384,
contextLength=128000, contextLength=128000,
costPer1kTokensInput=0.03, costPer1kTokensInput=0.03,
costPer1kTokensOutput=0.06, costPer1kTokensOutput=0.06,
@ -158,7 +158,9 @@ class AiOpenai(BaseConnectorAi):
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
payload = { payload = {
@ -226,8 +228,8 @@ class AiOpenai(BaseConnectorAi):
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
prompt = messages[0]["content"] if messages else "" prompt = messages[0]["content"] if messages else ""
imageData = options.get("imageData") imageData = getattr(options, "imageData", None)
mimeType = options.get("mimeType", "image/jpeg") mimeType = getattr(options, "mimeType", "image/jpeg")
logger.debug(f"Starting image analysis with query '{prompt}' for size {len(imageData)}B...") logger.debug(f"Starting image analysis with query '{prompt}' for size {len(imageData)}B...")
@ -261,10 +263,6 @@ class AiOpenai(BaseConnectorAi):
} }
] ]
# Use a vision-capable model for image analysis
# Override the model for vision tasks
visionModel = "gpt-4o" # or "gpt-4-vision-preview" depending on availability
# Use parameters from model # Use parameters from model
temperature = model.temperature temperature = model.temperature
# Don't set maxTokens - let the model use its full context length # Don't set maxTokens - let the model use its full context length

View file

@ -44,27 +44,29 @@ class AiPerplexity(BaseConnectorAi):
"""Get all available Perplexity models.""" """Get all available Perplexity models."""
return [ return [
AiModel( AiModel(
name="llama-3.1-sonar-large-128k-online", name="sonar",
displayName="Perplexity Llama 3.1 Sonar Large 128k", displayName="Perplexity Sonar",
connectorType="perplexity", connectorType="perplexity",
apiUrl="https://api.perplexity.ai/chat/completions", apiUrl="https://api.perplexity.ai/chat/completions",
temperature=0.2, temperature=0.2,
maxTokens=128000, maxTokens=4000,
contextLength=128000, contextLength=32000,
costPer1kTokensInput=0.005, costPer1kTokensInput=0.005,
costPer1kTokensOutput=0.005, costPer1kTokensOutput=0.005,
speedRating=8, speedRating=8,
qualityRating=8, qualityRating=8,
# capabilities removed (not used in business logic) # capabilities removed (not used in business logic)
functionCall=self.callAiBasic, functionCall=self.callWebOperation,
priority=PriorityEnum.BALANCED, priority=PriorityEnum.BALANCED,
processingMode=ProcessingModeEnum.ADVANCED, processingMode=ProcessingModeEnum.ADVANCED,
operationTypes=createOperationTypeRatings( operationTypes=createOperationTypeRatings(
(OperationTypeEnum.PLAN, 7), (OperationTypeEnum.WEB_RESEARCH, 8),
(OperationTypeEnum.DATA_ANALYSE, 8), (OperationTypeEnum.WEB_SEARCH, 9),
(OperationTypeEnum.DATA_GENERATE, 7) (OperationTypeEnum.WEB_CRAWL, 7),
(OperationTypeEnum.WEB_NEWS, 8),
(OperationTypeEnum.WEB_QUESTIONS, 9)
), ),
version="llama-3.1-sonar-large-128k-online", version="sonar",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005 calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005
), ),
AiModel( AiModel(
@ -73,8 +75,8 @@ class AiPerplexity(BaseConnectorAi):
connectorType="perplexity", connectorType="perplexity",
apiUrl="https://api.perplexity.ai/chat/completions", apiUrl="https://api.perplexity.ai/chat/completions",
temperature=0.2, temperature=0.2,
maxTokens=128000, maxTokens=4000,
contextLength=128000, contextLength=32000,
costPer1kTokensInput=0.01, costPer1kTokensInput=0.01,
costPer1kTokensOutput=0.01, costPer1kTokensOutput=0.01,
speedRating=6, # Slower due to AI analysis speedRating=6, # Slower due to AI analysis
@ -92,84 +94,6 @@ class AiPerplexity(BaseConnectorAi):
), ),
version="sonar-pro", version="sonar-pro",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01 calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01
),
AiModel(
name="mistral-7b-instruct",
displayName="Perplexity Mistral 7B Instruct",
connectorType="perplexity",
apiUrl="https://api.perplexity.ai/chat/completions",
temperature=0.2,
maxTokens=32000,
contextLength=32000,
costPer1kTokensInput=0.002,
costPer1kTokensOutput=0.002,
speedRating=9, # Fast for basic AI tasks
qualityRating=7, # Good but not premium quality
# capabilities removed (not used in business logic)
functionCall=self.callWebOperation,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.WEB_RESEARCH, 7),
(OperationTypeEnum.WEB_SEARCH, 6),
(OperationTypeEnum.WEB_CRAWL, 5),
(OperationTypeEnum.WEB_NEWS, 5),
(OperationTypeEnum.WEB_QUESTIONS, 6)
),
version="mistral-7b-instruct",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
),
AiModel(
name="mistral-7b-instruct-qa",
displayName="Perplexity Mistral 7B Instruct QA",
connectorType="perplexity",
apiUrl="https://api.perplexity.ai/chat/completions",
temperature=0.2,
maxTokens=32000,
contextLength=32000,
costPer1kTokensInput=0.002,
costPer1kTokensOutput=0.002,
speedRating=9, # Fast for Q&A tasks
qualityRating=7, # Good but not premium quality
# capabilities removed (not used in business logic)
functionCall=self.callWebOperation,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.WEB_RESEARCH, 6),
(OperationTypeEnum.WEB_SEARCH, 5),
(OperationTypeEnum.WEB_CRAWL, 4),
(OperationTypeEnum.WEB_NEWS, 4),
(OperationTypeEnum.WEB_QUESTIONS, 10)
),
version="mistral-7b-instruct",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
),
AiModel(
name="mistral-7b-instruct-news",
displayName="Perplexity Mistral 7B Instruct News",
connectorType="perplexity",
apiUrl="https://api.perplexity.ai/chat/completions",
temperature=0.2,
maxTokens=32000,
contextLength=32000,
costPer1kTokensInput=0.002,
costPer1kTokensOutput=0.002,
speedRating=9, # Fast for news tasks
qualityRating=7, # Good but not premium quality
# capabilities removed (not used in business logic)
functionCall=self.callWebOperation,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(
(OperationTypeEnum.WEB_RESEARCH, 6),
(OperationTypeEnum.WEB_SEARCH, 5),
(OperationTypeEnum.WEB_CRAWL, 4),
(OperationTypeEnum.WEB_NEWS, 10),
(OperationTypeEnum.WEB_QUESTIONS, 4)
),
version="mistral-7b-instruct",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
) )
] ]
@ -191,7 +115,9 @@ class AiPerplexity(BaseConnectorAi):
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
payload = { payload = {
@ -251,7 +177,9 @@ class AiPerplexity(BaseConnectorAi):
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
# Parse unified prompt JSON format # Parse unified prompt JSON format
@ -349,7 +277,9 @@ Include actual URLs in your response."""
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
payload = { payload = {
@ -408,7 +338,9 @@ Include actual URLs in your response."""
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
payload = { payload = {
@ -467,7 +399,9 @@ Include actual URLs in your response."""
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
payload = { payload = {
@ -526,7 +460,9 @@ Include actual URLs in your response."""
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
# Parse unified prompt JSON format # Parse unified prompt JSON format
@ -623,17 +559,17 @@ Extract content from each URL and provide detailed analysis."""
""" """
try: try:
options = modelCall.options options = modelCall.options
operationType = options.get("operationType") operationType = getattr(options, "operationType", None)
if operationType == "WEB_SEARCH": if operationType == OperationTypeEnum.WEB_SEARCH:
return await self.callAiWithWebSearch(modelCall) return await self.callAiWithWebSearch(modelCall)
elif operationType == "WEB_CRAWL": elif operationType == OperationTypeEnum.WEB_CRAWL:
return await self.crawl(modelCall) return await self.crawl(modelCall)
elif operationType == "WEB_RESEARCH": elif operationType == OperationTypeEnum.WEB_RESEARCH:
return await self.research(modelCall) return await self.research(modelCall)
elif operationType == "WEB_QUESTIONS": elif operationType == OperationTypeEnum.WEB_QUESTIONS:
return await self.questions(modelCall) return await self.questions(modelCall)
elif operationType == "WEB_NEWS": elif operationType == OperationTypeEnum.WEB_NEWS:
return await self.news(modelCall) return await self.news(modelCall)
else: else:
# Fallback to research for unknown operation types # Fallback to research for unknown operation types
@ -661,7 +597,9 @@ Extract content from each URL and provide detailed analysis."""
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
# Parse unified prompt JSON format # Parse unified prompt JSON format
@ -754,7 +692,9 @@ Provide comprehensive research with detailed analysis."""
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
# Parse unified prompt JSON format # Parse unified prompt JSON format
@ -850,7 +790,9 @@ Provide a detailed answer with well-cited sources."""
messages = modelCall.messages messages = modelCall.messages
model = modelCall.model model = modelCall.model
options = modelCall.options options = modelCall.options
temperature = options.get("temperature", model.temperature) temperature = getattr(options, "temperature", None)
if temperature is None:
temperature = model.temperature
maxTokens = model.maxTokens maxTokens = model.maxTokens
# Parse unified prompt JSON format # Parse unified prompt JSON format

View file

@ -64,6 +64,20 @@ class ConnectorWeb(BaseConnectorAi):
# Cached web search constraints (camelCase per project style) # Cached web search constraints (camelCase per project style)
self.webSearchMinResults: int = 1 self.webSearchMinResults: int = 1
self.webSearchMaxResults: int = 20 self.webSearchMaxResults: int = 20
# Initialize client if API key is available
self._initializeClient()
def _initializeClient(self):
"""Initialize the Tavily client if API key is available."""
try:
api_key = APP_CONFIG.get("Connector_AiTavily_API_SECRET")
if api_key:
self.client = AsyncTavilyClient(api_key=api_key)
logger.info("Tavily client initialized successfully")
else:
logger.warning("Tavily API key not found, client not initialized")
except Exception as e:
logger.error(f"Failed to initialize Tavily client: {str(e)}")
def getConnectorType(self) -> str: def getConnectorType(self) -> str:
"""Get the connector type identifier.""" """Get the connector type identifier."""
@ -442,13 +456,13 @@ One URL per line.
""" """
try: try:
options = modelCall.options options = modelCall.options
operationType = options.get("operationType") operationType = getattr(options, "operationType", None)
if operationType == "WEB_SEARCH": if operationType == OperationTypeEnum.WEB_SEARCH:
return await self.search(modelCall) return await self.search(modelCall)
elif operationType == "WEB_CRAWL": elif operationType == OperationTypeEnum.WEB_CRAWL:
return await self.crawl(modelCall) return await self.crawl(modelCall)
elif operationType in ["WEB_RESEARCH", "WEB_QUESTIONS", "WEB_NEWS"]: elif operationType in [OperationTypeEnum.WEB_RESEARCH, OperationTypeEnum.WEB_QUESTIONS, OperationTypeEnum.WEB_NEWS]:
return await self.research(modelCall) return await self.research(modelCall)
else: else:
# Fallback to search for unknown operation types # Fallback to search for unknown operation types
@ -493,8 +507,8 @@ One URL per line.
time_range=optimizedParams.get("time_range", timeRange), time_range=optimizedParams.get("time_range", timeRange),
country=optimizedParams.get("country", country), country=optimizedParams.get("country", country),
language=optimizedParams.get("language", language), language=optimizedParams.get("language", language),
include_answer=options.get("include_answer", True), include_answer=getattr(options, "include_answer", True),
include_raw_content=options.get("include_raw_content", True), include_raw_content=getattr(options, "include_raw_content", True),
) )
# Step 3: AI-based URL selection and intelligent filtering # Step 3: AI-based URL selection and intelligent filtering
@ -607,14 +621,14 @@ One URL per line.
# Extract parameters from modelCall # Extract parameters from modelCall
promptContent = modelCall.messages[0]["content"] if modelCall.messages else "" promptContent = modelCall.messages[0]["content"] if modelCall.messages else ""
options = modelCall.options options = modelCall.options
operationType = options.get("operationType") operationType = getattr(options, "operationType", None)
# Parse unified prompt JSON format # Parse unified prompt JSON format
import json import json
promptData = json.loads(promptContent) promptData = json.loads(promptContent)
# Extract parameters based on operation type # Extract parameters based on operation type
if operationType == "WEB_RESEARCH": if operationType == OperationTypeEnum.WEB_RESEARCH:
query = promptData.get("researchPrompt", promptContent) query = promptData.get("researchPrompt", promptContent)
maxResults = promptData.get("maxResults", 8) maxResults = promptData.get("maxResults", 8)
searchDepth = "basic" searchDepth = "basic"
@ -623,7 +637,7 @@ One URL per line.
language = promptData.get("language") language = promptData.get("language")
topic = "general" topic = "general"
elif operationType == "WEB_QUESTIONS": elif operationType == OperationTypeEnum.WEB_QUESTIONS:
query = promptData.get("question", promptContent) query = promptData.get("question", promptContent)
maxResults = promptData.get("maxResults", 6) maxResults = promptData.get("maxResults", 6)
searchDepth = "basic" searchDepth = "basic"
@ -632,7 +646,7 @@ One URL per line.
language = promptData.get("language") language = promptData.get("language")
topic = "general" topic = "general"
elif operationType == "WEB_NEWS": elif operationType == OperationTypeEnum.WEB_NEWS:
query = promptData.get("newsPrompt", promptContent) query = promptData.get("newsPrompt", promptContent)
maxResults = promptData.get("maxResults", 10) maxResults = promptData.get("maxResults", 10)
searchDepth = "basic" searchDepth = "basic"
@ -766,22 +780,22 @@ One URL per line.
search_results = await self._search( search_results = await self._search(
query=query, query=query,
max_results=options.get("max_results", 5), max_results=getattr(options, "max_results", 5),
search_depth=options.get("search_depth"), search_depth=getattr(options, "search_depth", None),
time_range=options.get("time_range"), time_range=getattr(options, "time_range", None),
topic=options.get("topic"), topic=getattr(options, "topic", None),
include_domains=options.get("include_domains"), include_domains=getattr(options, "include_domains", None),
exclude_domains=options.get("exclude_domains"), exclude_domains=getattr(options, "exclude_domains", None),
language=options.get("language"), language=getattr(options, "language", None),
include_answer=options.get("include_answer"), include_answer=getattr(options, "include_answer", None),
include_raw_content=options.get("include_raw_content"), include_raw_content=getattr(options, "include_raw_content", None),
) )
urls = [result.url for result in search_results] urls = [result.url for result in search_results]
crawl_results = await self._crawl( crawl_results = await self._crawl(
urls, urls,
extract_depth=options.get("extract_depth"), extract_depth=getattr(options, "extract_depth", None),
format=options.get("format"), format=getattr(options, "format", None),
) )
# Convert to JSON string # Convert to JSON string
@ -805,8 +819,8 @@ One URL per line.
success=True, success=True,
metadata={ metadata={
"total_count": len(crawl_results), "total_count": len(crawl_results),
"search_depth": options.get("search_depth", "basic"), "search_depth": getattr(options, "search_depth", "basic"),
"extract_depth": options.get("extract_depth", "basic") "extract_depth": getattr(options, "extract_depth", "basic")
} }
) )
@ -936,6 +950,13 @@ One URL per line.
kwargs["include_raw_content"] = include_raw_content kwargs["include_raw_content"] = include_raw_content
logger.debug(f"Tavily.search kwargs: {kwargs}") logger.debug(f"Tavily.search kwargs: {kwargs}")
# Ensure client is initialized
if self.client is None:
self._initializeClient()
if self.client is None:
raise ValueError("Tavily client not initialized. Please check API key configuration.")
response = await self.client.search(**kwargs) response = await self.client.search(**kwargs)
return [ return [
@ -973,6 +994,12 @@ One URL per line.
logger.debug(f"Sending request to Tavily with kwargs: {kwargs_extract}") logger.debug(f"Sending request to Tavily with kwargs: {kwargs_extract}")
# Ensure client is initialized
if self.client is None:
self._initializeClient()
if self.client is None:
raise ValueError("Tavily client not initialized. Please check API key configuration.")
response = await asyncio.wait_for( response = await asyncio.wait_for(
self.client.extract(**kwargs_extract), self.client.extract(**kwargs_extract),
timeout=timeout timeout=timeout

View file

@ -2,8 +2,8 @@ from typing import Optional, List, Dict, Any, Callable, TYPE_CHECKING, Tuple
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from enum import Enum from enum import Enum
if TYPE_CHECKING: # Import ContentPart for runtime use (needed for Pydantic model rebuilding)
from modules.datamodels.datamodelExtraction import ContentPart from modules.datamodels.datamodelExtraction import ContentPart
# Operation Types # Operation Types
class OperationTypeEnum(str, Enum): class OperationTypeEnum(str, Enum):
@ -173,7 +173,7 @@ class AiModelCall(BaseModel):
messages: List[Dict[str, Any]] = Field(description="Messages in OpenAI format (role, content)") 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") 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") options: AiCallOptions = Field(default_factory=AiCallOptions, description="Additional model-specific options")
class Config: class Config:
arbitraryTypesAllowed = True arbitraryTypesAllowed = True

View file

@ -399,9 +399,9 @@ class AiObjects:
inputBytes = len((prompt + context).encode('utf-8')) inputBytes = len((prompt + context).encode('utf-8'))
# Replace <TOKEN_LIMIT> placeholder in prompt for this specific model # Replace <TOKEN_LIMIT> placeholder in prompt for this specific model
contextLength = model.contextLength # Use maxTokens for output limit, not contextLength
if contextLength > 0: if model.maxTokens > 0:
tokenLimit = str(contextLength) tokenLimit = str(model.maxTokens)
else: else:
tokenLimit = "16000" # Default for text generation tokenLimit = "16000" # Default for text generation
@ -450,7 +450,7 @@ class AiObjects:
outputBytes = len(content.encode("utf-8")) outputBytes = len(content.encode("utf-8"))
# Calculate price using model's own price calculation method # Calculate price using model's own price calculation method
priceUsd = model.calculatePriceUsd(inputBytes, outputBytes) priceUsd = model.calculatePriceUsd(processingTime, inputBytes, outputBytes)
return AiCallResponse( return AiCallResponse(
content=content, content=content,
@ -542,7 +542,7 @@ class AiObjects:
modelCall = AiModelCall( modelCall = AiModelCall(
messages=[{"role": "user", "content": prompt}], messages=[{"role": "user", "content": prompt}],
model=model, model=model,
options={"imageData": imageData, "mimeType": mimeType} options=AiCallOptions(imageData=imageData, mimeType=mimeType)
) )
# Call the model with standardized interface # Call the model with standardized interface
@ -562,7 +562,7 @@ class AiObjects:
outputBytes = len(content.encode("utf-8")) outputBytes = len(content.encode("utf-8"))
# Calculate price using model's own price calculation method # Calculate price using model's own price calculation method
priceUsd = model.calculatePriceUsd(inputBytes, outputBytes) priceUsd = model.calculatePriceUsd(processingTime, inputBytes, outputBytes)
return AiCallResponse( return AiCallResponse(
content=content, content=content,
@ -603,7 +603,7 @@ class AiObjects:
modelCall = AiModelCall( modelCall = AiModelCall(
messages=[{"role": "user", "content": prompt}], messages=[{"role": "user", "content": prompt}],
model=selectedModel, model=selectedModel,
options={"size": size, "quality": quality, "style": style} options=AiCallOptions(size=size, quality=quality, style=style)
) )
# Call the model with standardized interface # Call the model with standardized interface
@ -623,13 +623,13 @@ class AiObjects:
outputBytes = len(content.encode("utf-8")) outputBytes = len(content.encode("utf-8"))
# Calculate price using model's own price calculation method # Calculate price using model's own price calculation method
priceUsd = selectedModel.calculatePriceUsd(inputBytes, outputBytes) priceUsd = selectedModel.calculatePriceUsd(processingTime, inputBytes, outputBytes)
logger.info(f"✅ Image generation successful with model: {modelName}") logger.info(f"✅ Image generation successful with model: {modelName}")
return AiCallResponse( return AiCallResponse(
success=True, success=True,
content=content, content=content,
model=modelName, modelName=modelName,
processingTime=processingTime, processingTime=processingTime,
priceUsd=priceUsd, priceUsd=priceUsd,
bytesSent=inputBytes, bytesSent=inputBytes,

View file

@ -8,6 +8,7 @@ from modules.interfaces.interfaceAiObjects import AiObjects
from modules.services.serviceAi.subCoreAi import SubCoreAi from modules.services.serviceAi.subCoreAi import SubCoreAi
from modules.services.serviceAi.subDocumentProcessing import SubDocumentProcessing from modules.services.serviceAi.subDocumentProcessing import SubDocumentProcessing
from modules.services.serviceAi.subDocumentGeneration import SubDocumentGeneration from modules.services.serviceAi.subDocumentGeneration import SubDocumentGeneration
from modules.services.serviceAi.subSharedAiUtils import sanitizePromptContent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -142,4 +143,8 @@ class AiService:
# Use "json" for document generation calls since they return JSON # Use "json" for document generation calls since they return JSON
return await self.coreAi.callAiDocuments(prompt, documents, options, outputFormat, title, "json") return await self.coreAi.callAiDocuments(prompt, documents, options, outputFormat, title, "json")
def sanitizePromptContent(self, content: str, contentType: str = "text") -> str:
"""Sanitize prompt content to prevent injection attacks and ensure safe presentation."""
return sanitizePromptContent(content, contentType)

View file

@ -80,12 +80,11 @@ class AIBehaviorTester:
# Use the AI service directly with the user prompt - it will build the generation prompt internally # Use the AI service directly with the user prompt - it will build the generation prompt internally
try: try:
# Use the existing AI service with JSON format - it handles looping internally # Use the existing AI service with JSON format - it handles looping internally
response = await self.services.ai.coreAi.callAiDocuments( response = await self.services.ai.callAiDocuments(
prompt=prompt, # Use the raw user prompt directly prompt=prompt, # Use the raw user prompt directly
documents=None, documents=None,
outputFormat="json", outputFormat="json",
title="Prime Numbers Test", title="Prime Numbers Test"
loopInstructionFormat="json" # Use the JSON loop instructions
) )
if isinstance(response, dict): if isinstance(response, dict):

583
test_ai_models.py Normal file
View file

@ -0,0 +1,583 @@
#!/usr/bin/env python3
"""
AI Models Test - Tests all available AI models individually
"""
import asyncio
import json
import sys
import os
import base64
from datetime import datetime
from typing import Dict, Any, List
# Add the gateway to path
sys.path.append(os.path.dirname(__file__))
# Import the service initialization
from modules.features.chatPlayground.mainChatPlayground import getServices
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
from modules.datamodels.datamodelUam import User
class AIModelsTester:
def __init__(self):
# Create a minimal user context for testing
testUser = User(
id="test_user",
username="test_user",
email="test@example.com",
fullName="Test User",
language="en",
mandateId="test_mandate"
)
# Initialize services using the existing system
self.services = getServices(testUser, None) # Test user, no workflow
self.testResults = []
# Create logs directory if it doesn't exist
self.logsDir = os.path.join(os.path.dirname(__file__), "..", "local", "logs")
os.makedirs(self.logsDir, exist_ok=True)
# Create modeltest subdirectory
self.modelTestDir = os.path.join(self.logsDir, "modeltest")
os.makedirs(self.modelTestDir, exist_ok=True)
# Copy test image to modeltest directory if it exists
testImageSource = os.path.join(self.logsDir, "_testdata_photo_2025-06-03_13-05-52.jpg")
testImageDest = os.path.join(self.modelTestDir, "_testdata_photo_2025-06-03_13-05-52.jpg")
if os.path.exists(testImageSource) and not os.path.exists(testImageDest):
import shutil
shutil.copy2(testImageSource, testImageDest)
print(f"📷 Test image copied to: {testImageDest}")
async def initialize(self):
"""Initialize the AI service."""
# Set logging level to INFO to reduce noise
import logging
logging.getLogger().setLevel(logging.INFO)
# The AI service needs to be recreated with proper initialization
from modules.services.serviceAi.mainServiceAi import AiService
self.services.ai = await AiService.create(self.services)
# Create a minimal workflow context
from modules.datamodels.datamodelChat import ChatWorkflow
import uuid
self.services.currentWorkflow = ChatWorkflow(
id=str(uuid.uuid4()),
name="Test Workflow",
status="running",
startedAt=self.services.utils.timestampGetUtc(),
lastActivity=self.services.utils.timestampGetUtc(),
currentRound=1,
currentTask=0,
currentAction=0,
totalTasks=0,
totalActions=0,
mandateId="test_mandate",
messageIds=[],
workflowMode="React",
maxSteps=5
)
print("✅ AI Service initialized successfully")
print(f"📁 Results will be saved to: {self.modelTestDir}")
async def testModel(self, modelName: str) -> Dict[str, Any]:
"""Test a specific AI model with a simple prompt."""
print(f"\n{'='*60}")
print(f"TESTING MODEL: {modelName}")
print(f"{'='*60}")
# Choose test prompt based on model type - Web models get JSON formatted prompts
import json
if "tavily" in modelName.lower():
# Tavily models get web search prompt in JSON format (from methodAi.py)
testPrompt = json.dumps({
"searchPrompt": "Search for recent news about artificial intelligence developments in 2024. Return the top 3 results as JSON with fields: title, url, snippet.",
"maxResults": 3,
"timeRange": "y",
"country": "United States",
"instructions": "Search the web and return a JSON response with a 'results' array containing objects with 'title', 'url', and optionally 'content' fields. Focus on finding relevant URLs for the search prompt."
}, indent=2)
elif "perplexity" in modelName.lower() or "llama" in modelName.lower() or "sonar" in modelName.lower() or "mistral" in modelName.lower():
# Perplexity models get web research prompt in JSON format (from methodAi.py)
testPrompt = json.dumps({
"researchPrompt": "Research the latest trends in renewable energy technology. Provide a comprehensive overview with key developments, companies involved, and future prospects. Return as JSON.",
"maxResults": 5,
"timeRange": "y",
"country": "United States",
"instructions": "Conduct comprehensive web research and return a JSON response with 'results' array containing objects with 'title', 'url', 'content', and 'analysis' fields. Provide detailed analysis and insights."
}, indent=2)
else:
# Fallback for other models
testPrompt = "Generate a comprehensive analysis of the current state of artificial intelligence. Return as JSON."
print(f"Test prompt: {testPrompt}")
print(f"Prompt length: {len(testPrompt)} characters")
startTime = asyncio.get_event_loop().time()
try:
# Create options to force this specific model
if "internal" in modelName.lower():
options = AiCallOptions(
operationType=OperationTypeEnum.DATA_EXTRACT,
preferredModel=modelName
)
else:
options = AiCallOptions(
operationType=OperationTypeEnum.DATA_GENERATE,
preferredModel=modelName
)
# Call the AI service DIRECTLY through the model's functionCall
# This tests the actual model, not the document generation pipeline
# Get the model directly from the registry using the model registry
from modules.aicore.aicoreModelRegistry import modelRegistry
model = modelRegistry.getModel(modelName)
if not model:
raise Exception(f"Model {modelName} not found")
# Create AiModelCall and call the model's functionCall directly
from modules.datamodels.datamodelAi import AiModelCall
import base64
import os
# Prepare messages and options based on model type
if "vision" in modelName.lower():
# For vision models, skip for now since they require special handling
print(f"⚠️ Skipping vision model {modelName} - requires special image handling")
return {
"modelName": modelName,
"status": "SKIPPED",
"processingTime": 0.0,
"responseLength": 0,
"responseType": "skipped",
"hasContent": False,
"error": "Vision model requires special image handling",
"fullResponse": "Skipped - vision model requires special image handling"
}
else:
# For other models, use normal functionCall
messages = [{"role": "user", "content": testPrompt}]
modelCall = AiModelCall(
messages=messages,
model=model,
options=options
)
response = await model.functionCall(modelCall)
endTime = asyncio.get_event_loop().time()
processingTime = endTime - startTime
# Analyze response - now we get AiModelResponse objects
if hasattr(response, 'success'):
# AiModelResponse object
if response.success:
result = {
"modelName": modelName,
"status": "SUCCESS",
"processingTime": round(processingTime, 2),
"responseLength": len(response.content) if response.content else 0,
"responseType": "AiModelResponse",
"hasContent": bool(response.content),
"error": None,
"modelUsed": modelName,
"priceUsd": 0.0, # AiModelResponse doesn't have price info
"bytesSent": 0,
"bytesReceived": len(response.content.encode('utf-8')) if response.content else 0
}
# Try to parse content as JSON
if response.content:
try:
json.loads(response.content)
result["isValidJson"] = True
except:
result["isValidJson"] = False
result["responsePreview"] = response.content[:200] + "..." if len(response.content) > 200 else response.content
result["fullResponse"] = response.content
else:
result["isValidJson"] = False
result["responsePreview"] = "Empty response"
result["fullResponse"] = ""
print(f"✅ SUCCESS - Processing time: {processingTime:.2f}s")
print(f"📄 Response length: {len(response.content) if response.content else 0} characters")
print(f"📄 Model used: {modelName}")
print(f"📄 Response preview: {result['responsePreview']}")
else:
error = response.error or "Unknown error"
result = {
"modelName": modelName,
"status": "ERROR",
"processingTime": round(processingTime, 2),
"responseLength": 0,
"responseType": "AiModelResponse",
"hasContent": False,
"error": error,
"fullResponse": str(response)
}
print(f"❌ ERROR - {error}")
elif isinstance(response, dict):
# Fallback for dict responses
if response.get("success", True):
result = {
"modelName": modelName,
"status": "SUCCESS",
"processingTime": round(processingTime, 2),
"responseLength": len(str(response)),
"responseType": "dict",
"hasContent": True,
"error": None
}
# Try to parse as JSON
try:
jsonResponse = json.dumps(response, indent=2)
result["responsePreview"] = jsonResponse[:200] + "..." if len(jsonResponse) > 200 else jsonResponse
result["isValidJson"] = True
result["fullResponse"] = jsonResponse
except:
result["responsePreview"] = str(response)[:200] + "..." if len(str(response)) > 200 else str(response)
result["isValidJson"] = False
result["fullResponse"] = str(response)
print(f"✅ SUCCESS - Processing time: {processingTime:.2f}s")
print(f"📄 Response length: {len(str(response))} characters")
print(f"📄 Response preview: {result['responsePreview']}")
else:
error = response.get("error", "Unknown error")
result = {
"modelName": modelName,
"status": "ERROR",
"processingTime": round(processingTime, 2),
"responseLength": 0,
"responseType": "error",
"hasContent": False,
"error": error,
"fullResponse": str(response)
}
print(f"❌ ERROR - {error}")
else:
# String response
result = {
"modelName": modelName,
"status": "SUCCESS",
"processingTime": round(processingTime, 2),
"responseLength": len(str(response)),
"responseType": "string",
"hasContent": True,
"error": None
}
# Try to parse as JSON
try:
json.loads(str(response))
result["isValidJson"] = True
except:
result["isValidJson"] = False
result["responsePreview"] = str(response)[:200] + "..." if len(str(response)) > 200 else str(response)
result["fullResponse"] = str(response)
print(f"✅ SUCCESS - Processing time: {processingTime:.2f}s")
print(f"📄 Response length: {len(str(response))} characters")
print(f"📄 Response preview: {result['responsePreview']}")
# Save text response for all models
if result.get("status") == "SUCCESS":
self._saveTextResponse(modelName, result)
except Exception as e:
endTime = asyncio.get_event_loop().time()
processingTime = endTime - startTime
result = {
"modelName": modelName,
"status": "EXCEPTION",
"processingTime": round(processingTime, 2),
"responseLength": 0,
"responseType": "exception",
"hasContent": False,
"error": str(e)
}
print(f"💥 EXCEPTION - {str(e)}")
self.testResults.append(result)
# Save individual model result immediately
self._saveIndividualModelResult(modelName, result)
return result
def _saveImageResponse(self, modelName: str, result: Dict[str, Any]):
"""Save base64 image response to file."""
try:
fullResponse = result.get("fullResponse", "")
base64Data = None
# Try to extract base64 data from response
if isinstance(fullResponse, dict):
# Look for base64 data in the response
if "content" in fullResponse:
base64Data = fullResponse["content"]
elif "data" in fullResponse:
base64Data = fullResponse["data"]
elif "image" in fullResponse:
base64Data = fullResponse["image"]
else:
# Try to find base64 data in string response
import re
base64Match = re.search(r'data:image/[^;]+;base64,([A-Za-z0-9+/=]+)', str(fullResponse))
if base64Match:
base64Data = base64Match.group(1)
else:
# Try to find pure base64 string
base64Match = re.search(r'([A-Za-z0-9+/=]{100,})', str(fullResponse))
if base64Match:
base64Data = base64Match.group(1)
if base64Data:
# Clean base64 data
if base64Data.startswith('data:image/'):
base64Data = base64Data.split(',', 1)[1]
# Decode and save image
imageData = base64.b64decode(base64Data)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{modelName}_{timestamp}.png"
filepath = os.path.join(self.modelTestDir, filename)
with open(filepath, 'wb') as f:
f.write(imageData)
result["savedImage"] = filepath
print(f"🖼️ Image saved: {filepath}")
else:
print(f"⚠️ No base64 image data found in response")
except Exception as e:
print(f"❌ Error saving image: {str(e)}")
result["imageSaveError"] = str(e)
def _saveTextResponse(self, modelName: str, result: Dict[str, Any]):
"""Save text response to file."""
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{modelName}_{timestamp}.txt"
filepath = os.path.join(self.modelTestDir, filename)
# Prepare content for saving
content = result.get("fullResponse", "")
if not content:
content = result.get("responsePreview", "No content available")
# Add metadata header
metadata = f"""Model: {modelName}
Test Time: {timestamp}
Status: {result.get('status', 'Unknown')}
Processing Time: {result.get('processingTime', 0):.2f}s
Response Length: {result.get('responseLength', 0)} characters
Is Valid JSON: {result.get('isValidJson', False)}
--- RESPONSE CONTENT ---
{content}
"""
with open(filepath, 'w', encoding='utf-8') as f:
f.write(metadata)
result["savedTextFile"] = filepath
print(f"📄 Text response saved: {filepath}")
except Exception as e:
print(f"❌ Error saving text response: {str(e)}")
result["textSaveError"] = str(e)
def _saveIndividualModelResult(self, modelName: str, result: Dict[str, Any]):
"""Save individual model test result to file."""
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{modelName}_{timestamp}.json"
filepath = os.path.join(self.modelTestDir, filename)
# Prepare individual result data
individualData = {
"modelName": modelName,
"testTimestamp": timestamp,
"testDate": datetime.now().isoformat(),
"result": result
}
# Save to JSON file
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(individualData, f, indent=2, ensure_ascii=False)
print(f"📄 Individual result saved: {filename}")
except Exception as e:
print(f"❌ Error saving individual result: {str(e)}")
def getAllAvailableModels(self) -> List[str]:
"""Get all available model names."""
# Hardcoded list of known models - same approach as test_ai_behavior.py
return [
# "claude-3-5-sonnet-20241022", # Skipped - text model, test later
# "claude-3-5-sonnet-20241022-vision", # Skipped - requires image input
# "gpt-4o", # Skipped - text model, test later
# "gpt-3.5-turbo", # Skipped - text model, test later
# "gpt-4o-vision", # Skipped - requires image input
# "dall-e-3", # Skipped - image generation, test later
"sonar", # Perplexity web model
"sonar-pro", # Perplexity web model
"tavily-search", # Tavily web model
"tavily-extract", # Tavily web model
"tavily-search-extract", # Tavily web model
# "internal-extractor", # Skipped - internal model, test later
# "internal-generator", # Skipped - internal model, test later
# "internal-renderer" # Skipped - internal model, test later
]
def saveTestResults(self):
"""Save detailed test results to file."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
resultsFile = os.path.join(self.modelTestDir, f"modeltest_results_{timestamp}.json")
# Prepare results for saving
saveData = {
"testTimestamp": timestamp,
"testDate": datetime.now().isoformat(),
"totalModels": len(self.testResults),
"successfulModels": len([r for r in self.testResults if r["status"] == "SUCCESS"]),
"errorModels": len([r for r in self.testResults if r["status"] == "ERROR"]),
"exceptionModels": len([r for r in self.testResults if r["status"] == "EXCEPTION"]),
"results": self.testResults
}
# Calculate success rate
if saveData["totalModels"] > 0:
saveData["successRate"] = (saveData["successfulModels"] / saveData["totalModels"]) * 100
else:
saveData["successRate"] = 0
# Save to JSON file
with open(resultsFile, 'w', encoding='utf-8') as f:
json.dump(saveData, f, indent=2, ensure_ascii=False)
print(f"📄 Detailed results saved: {resultsFile}")
return resultsFile
def printTestSummary(self):
"""Print a summary of all test results."""
print(f"\n{'='*80}")
print("AI MODELS TEST SUMMARY")
print(f"{'='*80}")
totalModels = len(self.testResults)
successfulModels = len([r for r in self.testResults if r["status"] == "SUCCESS"])
errorModels = len([r for r in self.testResults if r["status"] == "ERROR"])
exceptionModels = len([r for r in self.testResults if r["status"] == "EXCEPTION"])
print(f"📊 Total models tested: {totalModels}")
print(f"✅ Successful: {successfulModels}")
print(f"❌ Errors: {errorModels}")
print(f"💥 Exceptions: {exceptionModels}")
print(f"📈 Success rate: {(successfulModels/totalModels*100):.1f}%" if totalModels > 0 else "0%")
print(f"\n{'='*80}")
print("DETAILED RESULTS")
print(f"{'='*80}")
for result in self.testResults:
status_icon = {
"SUCCESS": "",
"ERROR": "",
"EXCEPTION": "💥"
}.get(result["status"], "")
print(f"\n{status_icon} {result['modelName']}")
print(f" Status: {result['status']}")
print(f" Processing time: {result['processingTime']}s")
print(f" Response length: {result['responseLength']} characters")
print(f" Response type: {result['responseType']}")
if result.get("isValidJson") is not None:
print(f" Valid JSON: {'Yes' if result['isValidJson'] else 'No'}")
if result["error"]:
print(f" Error: {result['error']}")
if result.get("responsePreview"):
print(f" Preview: {result['responsePreview']}")
# Find fastest and slowest models
if successfulModels > 0:
successfulResults = [r for r in self.testResults if r["status"] == "SUCCESS"]
fastest = min(successfulResults, key=lambda x: x["processingTime"])
slowest = max(successfulResults, key=lambda x: x["processingTime"])
print(f"\n{'='*80}")
print("PERFORMANCE HIGHLIGHTS")
print(f"{'='*80}")
print(f"🚀 Fastest model: {fastest['modelName']} ({fastest['processingTime']}s)")
print(f"🐌 Slowest model: {slowest['modelName']} ({slowest['processingTime']}s)")
async def main():
"""Run AI models testing."""
tester = AIModelsTester()
print("Starting AI Models Testing...")
print("Initializing AI service...")
await tester.initialize()
# Get all available models
models = tester.getAllAvailableModels()
print(f"\nFound {len(models)} models to test:")
for i, model in enumerate(models, 1):
print(f" {i}. {model}")
print(f"\n{'='*80}")
print("STARTING INDIVIDUAL MODEL TESTS")
print(f"{'='*80}")
print("Press Enter after each model test to continue to the next one...")
# Test each model individually
for i, modelName in enumerate(models, 1):
print(f"\n[{i}/{len(models)}] Testing model: {modelName}")
# Test the model
await tester.testModel(modelName)
# Pause for user input (except for the last model)
if i < len(models):
input(f"\nPress Enter to continue to the next model...")
# Save detailed results to file
resultsFile = tester.saveTestResults()
# Print final summary
tester.printTestSummary()
print(f"\n{'='*80}")
print("TESTING COMPLETED")
print(f"{'='*80}")
print(f"📄 Results saved to: {resultsFile}")
print(f"📁 Images saved to: {tester.modelTestDir}")
if __name__ == "__main__":
asyncio.run(main())