681 lines
30 KiB
Python
681 lines
30 KiB
Python
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, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
|
from modules.services.serviceAi.subSharedAiUtils import (
|
|
buildPromptWithPlaceholders,
|
|
extractTextFromContentParts,
|
|
reduceText,
|
|
determineCallType
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Rebuild the model to resolve forward references
|
|
AiCallRequest.model_rebuild()
|
|
|
|
|
|
# Loop instruction texts for different formats
|
|
LoopInstructionTexts = {
|
|
"json": """
|
|
CRITICAL LIMITS: <TOKEN_LIMIT> tokens total (reserve 20% for JSON structure)
|
|
|
|
MANDATORY RULES:
|
|
1. STOP at approximately 80% of limit to ensure valid JSON completion
|
|
2. Return ONLY raw JSON (no ```json blocks, no text before/after)
|
|
|
|
CONTINUATION REQUIREMENTS:
|
|
Refer to the json object below where to set the "continuation" information:
|
|
- If you can complete the full request: {"continuation": null}
|
|
- If you must stop early: {
|
|
"continuation": {
|
|
"last_data_items": "delivered last data for context (copy them)",
|
|
"next_instruction": "instruction for next data to deliver"
|
|
}
|
|
}
|
|
|
|
BE CONSERVATIVE: Stop generating content when you reach approximately 3200-3500 characters to ensure JSON completion.
|
|
""",
|
|
# Add more formats here as needed
|
|
# "xml": "...",
|
|
# "text": "...",
|
|
}
|
|
|
|
|
|
class SubCoreAi:
|
|
"""Core AI operations including image analysis, text generation, and planning calls."""
|
|
|
|
def __init__(self, services, aiObjects):
|
|
"""Initialize core AI operations.
|
|
|
|
Args:
|
|
services: Service center instance for accessing other services
|
|
aiObjects: Initialized AiObjects instance
|
|
"""
|
|
self.services = services
|
|
self.aiObjects = aiObjects
|
|
|
|
async def _analyzePromptAndCreateOptions(self, prompt: str) -> AiCallOptions:
|
|
"""Analyze prompt to determine appropriate AiCallOptions parameters."""
|
|
try:
|
|
# Get dynamic enum values from Pydantic models
|
|
operation_types = [e.value for e in OperationTypeEnum]
|
|
priorities = [e.value for e in PriorityEnum]
|
|
processing_modes = [e.value for e in ProcessingModeEnum]
|
|
|
|
# Create analysis prompt for AI to determine operation type and parameters
|
|
analysisPrompt = f"""
|
|
You are an AI operation analyzer. Analyze the following prompt and determine the most appropriate operation type and parameters.
|
|
|
|
PROMPT TO ANALYZE:
|
|
{self.services.ai.sanitizePromptContent(prompt, 'userinput')}
|
|
|
|
Based on the prompt content, determine:
|
|
1. operationType: Choose the most appropriate from: {', '.join(operation_types)}
|
|
2. priority: Choose from: {', '.join(priorities)}
|
|
3. processingMode: Choose from: {', '.join(processing_modes)}
|
|
4. compressPrompt: true/false (true for story-like prompts, false for structured prompts with JSON/schemas)
|
|
5. compressContext: true/false (true to summarize context, false to process fully)
|
|
|
|
Respond with ONLY a JSON object in this exact format:
|
|
{{
|
|
"operationType": "dataAnalyse",
|
|
"priority": "balanced",
|
|
"processingMode": "basic",
|
|
"compressPrompt": true,
|
|
"compressContext": true
|
|
}}
|
|
"""
|
|
|
|
# Use AI to analyze the prompt
|
|
request = AiCallRequest(
|
|
prompt=analysisPrompt,
|
|
options=AiCallOptions(
|
|
operationType=OperationTypeEnum.DATA_ANALYSE,
|
|
priority=PriorityEnum.SPEED,
|
|
processingMode=ProcessingModeEnum.BASIC,
|
|
compressPrompt=True,
|
|
compressContext=False
|
|
)
|
|
)
|
|
|
|
response = await self.aiObjects.call(request)
|
|
|
|
# Parse AI response
|
|
try:
|
|
import json
|
|
json_start = response.content.find('{')
|
|
json_end = response.content.rfind('}') + 1
|
|
if json_start != -1 and json_end > json_start:
|
|
analysis = json.loads(response.content[json_start:json_end])
|
|
|
|
# Map string values to enums
|
|
operation_type = OperationTypeEnum(analysis.get('operationType', 'dataAnalyse'))
|
|
priority = PriorityEnum(analysis.get('priority', 'balanced'))
|
|
processing_mode = ProcessingModeEnum(analysis.get('processingMode', 'basic'))
|
|
|
|
return AiCallOptions(
|
|
operationType=operation_type,
|
|
priority=priority,
|
|
processingMode=processing_mode,
|
|
compressPrompt=analysis.get('compressPrompt', True),
|
|
compressContext=analysis.get('compressContext', True)
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to parse AI analysis response: {e}")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Prompt analysis failed: {e}")
|
|
|
|
# Fallback to default options
|
|
return AiCallOptions(
|
|
operationType=OperationTypeEnum.DATA_ANALYSE,
|
|
priority=PriorityEnum.BALANCED,
|
|
processingMode=ProcessingModeEnum.BASIC
|
|
)
|
|
|
|
|
|
|
|
# Shared Core Function for AI Calls with Looping
|
|
async def _callAiWithLooping(
|
|
self,
|
|
prompt: str,
|
|
options: AiCallOptions,
|
|
debugPrefix: str = "ai_call",
|
|
loopInstructionFormat: str = None
|
|
) -> str:
|
|
"""
|
|
Shared core function for AI calls with looping system.
|
|
Handles continuation logic when response needs multiple rounds.
|
|
Delivers prompt and response to debug file log.
|
|
|
|
Args:
|
|
prompt: The prompt to send to AI
|
|
options: AI call configuration options
|
|
debugPrefix: Prefix for debug file names
|
|
loopInstructionFormat: If provided, replaces LOOP_INSTRUCTION placeholder and includes in continuation prompts
|
|
|
|
Returns:
|
|
Complete AI response after all iterations
|
|
"""
|
|
max_iterations = 100 # Prevent infinite loops
|
|
iteration = 0
|
|
accumulatedContent = []
|
|
|
|
logger.debug(f"Starting AI call with looping (debug prefix: {debugPrefix}, loopInstructionFormat: {loopInstructionFormat is not None})")
|
|
|
|
|
|
# Determine loopInstruction based on loopInstructionFormat (before iterations)
|
|
if not loopInstructionFormat:
|
|
loopInstruction = ""
|
|
elif loopInstructionFormat in LoopInstructionTexts:
|
|
loopInstruction = LoopInstructionTexts[loopInstructionFormat]
|
|
else:
|
|
logger.error(f"Unsupported loopInstructionFormat for prompt: {loopInstructionFormat}")
|
|
loopInstruction = ""
|
|
|
|
|
|
while iteration < max_iterations:
|
|
iteration += 1
|
|
logger.debug(f"AI call iteration {iteration}/{max_iterations}")
|
|
|
|
# Build iteration prompt
|
|
if iteration == 1:
|
|
if "LOOP_INSTRUCTION" in prompt:
|
|
iterationPrompt = prompt.replace("LOOP_INSTRUCTION", loopInstruction)
|
|
else:
|
|
iterationPrompt = prompt
|
|
elif loopInstruction and iteration > 1:
|
|
continuationContent = self._buildContinuationContent(accumulatedContent, iteration)
|
|
if "LOOP_INSTRUCTION" in prompt:
|
|
iterationPrompt = prompt.replace("LOOP_INSTRUCTION", f"{continuationContent}\n\n{loopInstruction}")
|
|
else:
|
|
iterationPrompt = prompt
|
|
else:
|
|
iterationPrompt = prompt
|
|
|
|
# Make AI call
|
|
try:
|
|
from modules.datamodels.datamodelAi import AiCallRequest
|
|
request = AiCallRequest(
|
|
prompt=iterationPrompt,
|
|
context="",
|
|
options=options
|
|
)
|
|
|
|
# Write the ACTUAL prompt sent to AI (including continuation context)
|
|
if iteration == 1:
|
|
# First iteration - use the historic naming pattern
|
|
self.services.utils.writeDebugFile(iterationPrompt, f"{debugPrefix}_prompt")
|
|
else:
|
|
# Subsequent iterations - include iteration number
|
|
self.services.utils.writeDebugFile(iterationPrompt, f"{debugPrefix}_prompt_iteration_{iteration}")
|
|
|
|
response = await self.aiObjects.call(request)
|
|
result = response.content
|
|
|
|
# Write raw AI response to debug file
|
|
if iteration == 1:
|
|
# First iteration - use the historic naming pattern
|
|
self.services.utils.writeDebugFile(result, f"{debugPrefix}_response")
|
|
else:
|
|
# Subsequent iterations - include iteration number
|
|
self.services.utils.writeDebugFile(result, f"{debugPrefix}_response_iteration_{iteration}")
|
|
|
|
# Emit stats for this iteration
|
|
self.services.workflow.storeWorkflowStat(
|
|
self.services.currentWorkflow,
|
|
response,
|
|
f"ai.call.{debugPrefix}.iteration_{iteration}"
|
|
)
|
|
|
|
if not result or not result.strip():
|
|
logger.warning(f"Iteration {iteration}: Empty response, stopping")
|
|
break
|
|
|
|
# Check if this is a continuation response (only for supported formats)
|
|
if loopInstructionFormat in LoopInstructionTexts:
|
|
try:
|
|
# Extract JSON substring if wrapped (e.g., ```json ... ```)
|
|
extracted = self.services.utils.jsonExtractString(result)
|
|
# Try to parse as JSON to check for continuation attribute
|
|
parsed_result = json.loads(extracted)
|
|
if isinstance(parsed_result, dict) and parsed_result.get("continuation") is not None:
|
|
# This is a continuation response
|
|
accumulatedContent.append(result)
|
|
logger.debug(f"Iteration {iteration}: Continuation detected in JSON, continuing...")
|
|
continue
|
|
else:
|
|
# This is the final response (continuation is null or missing)
|
|
accumulatedContent.append(result)
|
|
logger.debug(f"Iteration {iteration}: Final response received")
|
|
break
|
|
except json.JSONDecodeError:
|
|
# Not JSON, treat as final response
|
|
accumulatedContent.append(result)
|
|
logger.warning(f"Iteration {iteration}: Non-JSON response received")
|
|
self.services.utils.writeDebugFile(result, f"{debugPrefix}_error_non_json_response_iteration_{iteration}")
|
|
break
|
|
else:
|
|
# This is the final response
|
|
accumulatedContent.append(result)
|
|
logger.debug(f"Iteration {iteration}: Final response received")
|
|
break
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in AI call iteration {iteration}: {str(e)}")
|
|
break
|
|
|
|
if iteration >= max_iterations:
|
|
logger.warning(f"AI call stopped after maximum iterations ({max_iterations})")
|
|
|
|
# Intelligently merge JSON content from all iterations
|
|
final_result = self._mergeJsonContent(accumulatedContent) if accumulatedContent else ""
|
|
|
|
# Write final result to debug file
|
|
self.services.utils.writeDebugFile(final_result, f"{debugPrefix}_final_result")
|
|
|
|
logger.info(f"AI call completed: {len(accumulatedContent)} parts from {iteration} iterations")
|
|
return final_result
|
|
|
|
def _buildContinuationContent(
|
|
self,
|
|
accumulatedContent: List[str],
|
|
iteration: int
|
|
) -> str:
|
|
"""
|
|
Build continuation content for follow-up iterations.
|
|
"""
|
|
# Extract continuation description from the last response
|
|
continuation_description = ""
|
|
if accumulatedContent:
|
|
try:
|
|
last_response = accumulatedContent[-1]
|
|
# Use the same JSON extraction logic as the main loop
|
|
extracted = self.services.utils.jsonExtractString(last_response)
|
|
parsed_response = json.loads(extracted)
|
|
if isinstance(parsed_response, dict):
|
|
# Check for continuation at root level or in metadata
|
|
continuation = parsed_response.get("continuation")
|
|
if continuation is None and "metadata" in parsed_response:
|
|
continuation = parsed_response["metadata"].get("continuation")
|
|
|
|
if continuation:
|
|
continuation_description = continuation
|
|
except (json.JSONDecodeError, KeyError, ValueError):
|
|
pass
|
|
|
|
# Extract specific attributes from continuation object
|
|
last_data_items = ""
|
|
next_instruction = ""
|
|
|
|
if continuation_description:
|
|
try:
|
|
if isinstance(continuation_description, str):
|
|
continuation_obj = json.loads(continuation_description)
|
|
else:
|
|
continuation_obj = continuation_description
|
|
|
|
if isinstance(continuation_obj, dict):
|
|
last_data_items = continuation_obj.get("last_data_items", "")
|
|
next_instruction = continuation_obj.get("next_instruction", "")
|
|
except (json.JSONDecodeError, TypeError):
|
|
pass
|
|
|
|
continuation_content = f"""CONTINUATION REQUEST (Iteration {iteration}):
|
|
You are continuing a previous response. DO NOT repeat any previous content.
|
|
|
|
{f"Already delivered data: {last_data_items}" if last_data_items else "No previous data specified"}
|
|
|
|
{f"Your task to deliver: {next_instruction}" if next_instruction else "No specific task provided"}
|
|
|
|
CRITICAL REQUIREMENTS:
|
|
- Start from the exact point specified in continuation instructions
|
|
- DO NOT repeat any previous content
|
|
- BE CONSERVATIVE: Stop at approximately 3200-3500 characters to ensure JSON completion
|
|
- ALWAYS include continuation field - set to null if complete, or provide next instruction if incomplete
|
|
"""
|
|
return continuation_content
|
|
|
|
def _mergeJsonContent(self, accumulatedContent: List[str]) -> str:
|
|
"""
|
|
Generic JSON merger that combines all lists from multiple iterations.
|
|
Structure: root attributes + 1..n lists that get merged together.
|
|
"""
|
|
if not accumulatedContent:
|
|
return ""
|
|
|
|
if len(accumulatedContent) == 1:
|
|
return accumulatedContent[0]
|
|
|
|
try:
|
|
|
|
# Parse all JSON responses
|
|
parsed_responses = []
|
|
for content in accumulatedContent:
|
|
try:
|
|
extracted = self.services.utils.jsonExtractString(content)
|
|
parsed = json.loads(extracted)
|
|
parsed_responses.append(parsed)
|
|
except json.JSONDecodeError as e:
|
|
logger.warning(f"Failed to parse JSON content: {str(e)}")
|
|
continue
|
|
|
|
if not parsed_responses:
|
|
return accumulatedContent[0] # Return first response if all parsing failed
|
|
|
|
# Start with first response as base
|
|
merged = parsed_responses[0].copy()
|
|
|
|
# Merge all lists from all responses
|
|
for response in parsed_responses[1:]:
|
|
for key, value in response.items():
|
|
if isinstance(value, list) and key in merged and isinstance(merged[key], list):
|
|
# Merge lists by extending
|
|
merged[key].extend(value)
|
|
elif key not in merged:
|
|
# Add new fields
|
|
merged[key] = value
|
|
|
|
# Mark as complete
|
|
merged["continuation"] = None
|
|
|
|
return json.dumps(merged, indent=2)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error merging JSON content: {str(e)}")
|
|
return accumulatedContent[0] # Return first response on error
|
|
|
|
async def _buildGenerationPrompt(
|
|
self,
|
|
prompt: str,
|
|
extracted_content: Optional[str],
|
|
outputFormat: str,
|
|
title: str
|
|
) -> str:
|
|
"""
|
|
Build generation prompt for document generation.
|
|
"""
|
|
from modules.services.serviceGeneration.subPromptBuilder import buildGenerationPrompt
|
|
|
|
# Build the generation prompt using the existing system
|
|
generation_prompt = await buildGenerationPrompt(
|
|
outputFormat=outputFormat,
|
|
userPrompt=prompt,
|
|
title=title
|
|
)
|
|
|
|
# If we have extracted content, prepend it to the prompt
|
|
if extracted_content:
|
|
generation_prompt = f"""EXTRACTED CONTENT FROM DOCUMENTS:
|
|
{extracted_content}
|
|
|
|
{generation_prompt}"""
|
|
|
|
return generation_prompt
|
|
|
|
# Planning AI Call
|
|
async def callAiPlanning(
|
|
self,
|
|
prompt: str,
|
|
placeholders: Optional[List[PromptPlaceholder]] = None,
|
|
loopInstructionFormat: Optional[str] = None
|
|
) -> str:
|
|
"""
|
|
Planning AI call for task planning, action planning, action selection, etc.
|
|
Always uses static parameters optimized for planning tasks.
|
|
|
|
Args:
|
|
prompt: The planning prompt
|
|
placeholders: Optional list of placeholder replacements
|
|
loopInstructionFormat: Optional loop instruction format
|
|
|
|
Returns:
|
|
Planning JSON response
|
|
"""
|
|
# Planning calls always use static parameters
|
|
logger.debug("Using static parameters for planning call")
|
|
options = AiCallOptions(
|
|
operationType=OperationTypeEnum.PLAN,
|
|
priority=PriorityEnum.QUALITY,
|
|
processingMode=ProcessingModeEnum.DETAILED,
|
|
compressPrompt=False,
|
|
compressContext=False
|
|
)
|
|
|
|
# Build full prompt with placeholders
|
|
if placeholders:
|
|
placeholders_dict = {p.label: p.content for p in placeholders}
|
|
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, "plan", loopInstructionFormat=loopInstructionFormat)
|
|
|
|
# Document Generation AI Call
|
|
async def callAiDocuments(
|
|
self,
|
|
prompt: str,
|
|
documents: Optional[List[ChatDocument]] = None,
|
|
options: Optional[AiCallOptions] = None,
|
|
outputFormat: Optional[str] = None,
|
|
title: Optional[str] = None,
|
|
loopInstructionFormat: Optional[str] = None
|
|
) -> Union[str, Dict[str, Any]]:
|
|
"""
|
|
Document generation AI call for all non-planning calls.
|
|
Uses the current unified path with extraction and generation.
|
|
|
|
Args:
|
|
prompt: The main prompt for the AI call
|
|
documents: Optional list of documents to process
|
|
options: AI call configuration options
|
|
outputFormat: Optional output format for document generation
|
|
title: Optional title for generated documents
|
|
|
|
Returns:
|
|
AI response as string, or dict with documents if outputFormat is specified
|
|
"""
|
|
if options is None or (hasattr(options, 'operationType') and options.operationType is None):
|
|
# Use AI to determine parameters ONLY when truly needed (options=None OR operationType=None)
|
|
logger.debug("Analyzing prompt to determine optimal parameters")
|
|
options = await self._analyzePromptAndCreateOptions(prompt)
|
|
else:
|
|
logger.debug(f"Using provided options: operationType={options.operationType}, priority={options.priority}")
|
|
|
|
# Handle document generation with specific output format using unified approach
|
|
if outputFormat:
|
|
# Use unified generation method for all document generation
|
|
if documents and len(documents) > 0:
|
|
logger.debug(f"Extracting content from {len(documents)} documents")
|
|
extracted_content = await self.services.ai.documentProcessor.callAiText(prompt, documents, options)
|
|
else:
|
|
logger.debug("No documents provided - using direct generation")
|
|
extracted_content = None
|
|
generation_prompt = await self._buildGenerationPrompt(prompt, extracted_content, outputFormat, title)
|
|
generated_json = await self._callAiWithLooping(generation_prompt, options, "document_generation", loopInstructionFormat=loopInstructionFormat)
|
|
|
|
# Parse the generated JSON (extract fenced/embedded JSON first)
|
|
try:
|
|
extracted_json = self.services.utils.jsonExtractString(generated_json)
|
|
generated_data = json.loads(extracted_json)
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"Failed to parse generated JSON: {str(e)}")
|
|
logger.error(f"JSON content length: {len(generated_json)}")
|
|
logger.error(f"JSON content preview (last 200 chars): ...{generated_json[-200:]}")
|
|
logger.error(f"JSON content around error position: {generated_json[max(0, e.pos-50):e.pos+50]}")
|
|
|
|
# Write the problematic JSON to debug file
|
|
self.services.utils.writeDebugFile(generated_json, "failed_json_parsing")
|
|
|
|
return {"success": False, "error": f"Generated content is not valid JSON: {str(e)}"}
|
|
|
|
# Render to final format using the existing renderer
|
|
try:
|
|
from modules.services.serviceGeneration.mainServiceGeneration import GenerationService
|
|
generationService = GenerationService(self.services)
|
|
rendered_content, mime_type = await generationService.renderReport(
|
|
generated_data, outputFormat, title or "Generated Document", prompt, self
|
|
)
|
|
|
|
# Build result in the expected format
|
|
result = {
|
|
"success": True,
|
|
"content": generated_data,
|
|
"documents": [{
|
|
"documentName": f"generated.{outputFormat}",
|
|
"documentData": rendered_content,
|
|
"mimeType": mime_type,
|
|
"title": title or "Generated Document"
|
|
}],
|
|
"is_multi_file": False,
|
|
"format": outputFormat,
|
|
"title": title,
|
|
"split_strategy": "single",
|
|
"total_documents": 1,
|
|
"processed_documents": 1
|
|
}
|
|
|
|
# Log AI response for debugging
|
|
self.services.utils.writeDebugFile(str(result), "document_generation_response", documents)
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error rendering document: {str(e)}")
|
|
return {"success": False, "error": f"Rendering failed: {str(e)}"}
|
|
|
|
# Handle text calls (no output format specified)
|
|
if documents:
|
|
# Use document processing for text calls with documents
|
|
result = await self.services.ai.documentProcessor.callAiText(prompt, documents, options)
|
|
else:
|
|
# Use shared core function for direct text calls
|
|
result = await self._callAiWithLooping(prompt, options, "text", loopInstructionFormat=None)
|
|
|
|
return result
|
|
|
|
|
|
# AI Image Analysis
|
|
async def readImage(
|
|
self,
|
|
prompt: str,
|
|
imageData: Union[str, bytes],
|
|
mimeType: str = None,
|
|
options: Optional[AiCallOptions] = None,
|
|
) -> str:
|
|
"""Call AI for image analysis using interface.call() with contentParts."""
|
|
try:
|
|
# Check if imageData is valid
|
|
if not imageData:
|
|
error_msg = "No image data provided"
|
|
self.services.utils.debugLogToFile(f"Error in AI image analysis: {error_msg}", "AI_SERVICE")
|
|
logger.error(f"Error in AI image analysis: {error_msg}")
|
|
return f"Error: {error_msg}"
|
|
|
|
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_ANALYSE operation type for image processing
|
|
if options is None:
|
|
options = AiCallOptions(operationType=OperationTypeEnum.IMAGE_ANALYSE)
|
|
else:
|
|
# Override the operation type to ensure image analysis
|
|
options.operationType = OperationTypeEnum.IMAGE_ANALYSE
|
|
|
|
# Create content parts with image data
|
|
from modules.datamodels.datamodelExtraction import ContentPart
|
|
import base64
|
|
|
|
# ContentPart.data must be a string - convert bytes to base64 if needed
|
|
if isinstance(imageData, bytes):
|
|
imageDataStr = base64.b64encode(imageData).decode('utf-8')
|
|
else:
|
|
# Already a base64 string
|
|
imageDataStr = imageData
|
|
|
|
imagePart = ContentPart(
|
|
id="image_0",
|
|
parentId=None,
|
|
label="Image",
|
|
typeGroup="image",
|
|
mimeType=mimeType or "image/jpeg",
|
|
data=imageDataStr, # Must be a string (base64 encoded)
|
|
metadata={"imageAnalysis": True}
|
|
)
|
|
|
|
# Create request with content parts
|
|
from modules.datamodels.datamodelAi import AiCallRequest
|
|
request = AiCallRequest(
|
|
prompt=prompt,
|
|
context="",
|
|
options=options,
|
|
contentParts=[imagePart]
|
|
)
|
|
|
|
self.services.utils.debugLogToFile(f"Calling aiObjects.call() with operationType: {options.operationType}", "AI_SERVICE")
|
|
logger.info(f"Calling aiObjects.call() with operationType: {options.operationType}")
|
|
|
|
# Write image analysis prompt to debug file
|
|
self.services.utils.writeDebugFile(prompt, "image_analysis_prompt")
|
|
|
|
response = await self.aiObjects.call(request)
|
|
|
|
# Write image analysis response to debug file
|
|
# response is an AiCallResponse object
|
|
result = response.content
|
|
self.services.utils.writeDebugFile(result, "image_analysis_response")
|
|
|
|
# Debug the result
|
|
self.services.utils.debugLogToFile(f"AI image analysis result type: {type(response)}, content length: {len(result)}", "AI_SERVICE")
|
|
|
|
# Check if result is valid
|
|
if not result or (isinstance(result, str) and not result.strip()):
|
|
error_msg = f"No response from AI image analysis (result: {repr(result)})"
|
|
self.services.utils.debugLogToFile(f"Error in AI image analysis: {error_msg}", "AI_SERVICE")
|
|
logger.error(f"Error in AI image analysis: {error_msg}")
|
|
return f"Error: {error_msg}"
|
|
|
|
self.services.utils.debugLogToFile(f"callImage returned: {result[:200]}..." if len(result) > 200 else result, "AI_SERVICE")
|
|
logger.info(f"callImage returned: {result[:200]}..." if len(result) > 200 else result)
|
|
return result
|
|
except Exception as e:
|
|
self.services.utils.debugLogToFile(f"Error in AI image analysis: {str(e)}", "AI_SERVICE")
|
|
logger.error(f"Error in AI image analysis: {str(e)}")
|
|
return f"Error: {str(e)}"
|
|
|
|
# AI Image Generation
|
|
async def generateImage(
|
|
self,
|
|
prompt: str,
|
|
size: str = "1024x1024",
|
|
quality: str = "standard",
|
|
style: str = "vivid",
|
|
options: Optional[AiCallOptions] = None,
|
|
) -> Dict[str, Any]:
|
|
"""Generate an image using AI using interface.generateImage()."""
|
|
try:
|
|
response = await self.aiObjects.generateImage(prompt, size, quality, style, options)
|
|
|
|
# Emit stats for image generation
|
|
self.services.workflow.storeWorkflowStat(
|
|
self.services.currentWorkflow,
|
|
response,
|
|
f"ai.generate.image"
|
|
)
|
|
|
|
# Convert response to dict format for backward compatibility
|
|
if hasattr(response, 'content'):
|
|
return {
|
|
"success": True,
|
|
"content": response.content,
|
|
"modelName": response.modelName,
|
|
"priceUsd": response.priceUsd,
|
|
"processingTime": response.processingTime
|
|
}
|
|
else:
|
|
return response
|
|
except Exception as e:
|
|
logger.error(f"Error in AI image generation: {str(e)}")
|
|
return {"success": False, "error": str(e)}
|