streamlined core ai system to planning and documentation agents
This commit is contained in:
parent
11522bd763
commit
e368819b1b
20 changed files with 377 additions and 919 deletions
|
|
@ -978,7 +978,7 @@ class ChatObjects:
|
|||
def _storeDebugMessageAndDocuments(self, message: ChatMessage) -> None:
|
||||
"""
|
||||
Store message and documents (metadata and file bytes) for debugging purposes.
|
||||
Structure: gateway/test-chat/messages/m_round_task_action_timestamp/documentlist_label/
|
||||
Structure: {log_dir}/debug/messages/m_round_task_action_timestamp/documentlist_label/
|
||||
- message.json, message_text.txt
|
||||
- document_###_metadata.json
|
||||
- document_###_<original_filename> (actual file bytes)
|
||||
|
|
@ -992,7 +992,13 @@ class ChatObjects:
|
|||
from datetime import datetime, UTC
|
||||
|
||||
# Create base debug directory
|
||||
debug_root = "./test-chat/messages"
|
||||
# Use configured log directory instead of hardcoded test-chat
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./")
|
||||
if not os.path.isabs(logDir):
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
logDir = os.path.join(gatewayDir, logDir)
|
||||
debug_root = os.path.join(logDir, 'debug', 'messages')
|
||||
os.makedirs(debug_root, exist_ok=True)
|
||||
|
||||
# Generate timestamp
|
||||
|
|
|
|||
|
|
@ -153,43 +153,6 @@ class AiService:
|
|||
await self._ensureAiObjectsInitialized()
|
||||
return await self.webResearchService.webResearch(request)
|
||||
|
||||
# Master AI Call (process user prompt with optional unlimited count of input documents delivering one or many output documents, no size limitations)
|
||||
async def callAi(
|
||||
self,
|
||||
prompt: str,
|
||||
documents: Optional[List[ChatDocument]] = None,
|
||||
placeholders: Optional[List[PromptPlaceholder]] = None,
|
||||
options: Optional[AiCallOptions] = None,
|
||||
outputFormat: Optional[str] = None,
|
||||
title: Optional[str] = None
|
||||
) -> Union[str, Dict[str, Any]]:
|
||||
"""
|
||||
Unified AI call interface that automatically routes to appropriate handler.
|
||||
|
||||
Args:
|
||||
prompt: The main prompt for the AI call
|
||||
documents: Optional list of documents to process
|
||||
placeholders: Optional list of placeholder replacements for planning calls
|
||||
options: AI call configuration options
|
||||
outputFormat: Optional output format (html, pdf, docx, txt, md, json, csv, xlsx) for document generation
|
||||
title: Optional title for generated documents
|
||||
|
||||
Returns:
|
||||
AI response as string, or dict with documents if outputFormat is specified
|
||||
|
||||
Raises:
|
||||
Exception: If all available models fail
|
||||
"""
|
||||
await self._ensureAiObjectsInitialized()
|
||||
|
||||
# Get document processor and generator
|
||||
documentProcessor = self.documentProcessor
|
||||
documentGenerator = self.documentGenerator
|
||||
|
||||
return await self.coreAi.callAi(
|
||||
prompt, documents, placeholders, options, outputFormat, title,
|
||||
documentProcessor, documentGenerator
|
||||
)
|
||||
|
||||
def sanitizePromptContent(self, content: str, contentType: str = "text") -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -20,12 +20,211 @@ class SubCoreAi:
|
|||
self.services = services
|
||||
self.aiObjects = aiObjects
|
||||
|
||||
# AI Processing Call
|
||||
async def callAi(
|
||||
# Shared Core Function for AI Calls with Looping
|
||||
async def _callAiWithLooping(
|
||||
self,
|
||||
prompt: str,
|
||||
options: AiCallOptions,
|
||||
debug_prefix: str = "ai_call"
|
||||
) -> 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
|
||||
debug_prefix: Prefix for debug file names
|
||||
|
||||
Returns:
|
||||
Complete AI response after all iterations
|
||||
"""
|
||||
max_iterations = 10 # Prevent infinite loops
|
||||
iteration = 0
|
||||
accumulated_content = []
|
||||
|
||||
logger.info(f"Starting AI call with looping (debug prefix: {debug_prefix})")
|
||||
|
||||
# Write initial prompt to debug file
|
||||
from modules.shared.debugLogger import writeDebugFile
|
||||
writeDebugFile(prompt, f"{debug_prefix}_prompt", None)
|
||||
|
||||
while iteration < max_iterations:
|
||||
iteration += 1
|
||||
logger.info(f"AI call iteration {iteration}/{max_iterations}")
|
||||
|
||||
# Build iteration prompt
|
||||
if iteration == 1:
|
||||
iteration_prompt = prompt
|
||||
else:
|
||||
iteration_prompt = self._buildContinuationPrompt(prompt, accumulated_content, iteration)
|
||||
|
||||
# Make AI call
|
||||
try:
|
||||
from modules.datamodels.datamodelAi import AiCallRequest
|
||||
request = AiCallRequest(
|
||||
prompt=iteration_prompt,
|
||||
context="",
|
||||
options=options
|
||||
)
|
||||
response = await self.aiObjects.call(request)
|
||||
result = response.content
|
||||
|
||||
# Write raw AI response to debug file
|
||||
writeDebugFile(result, f"{debug_prefix}_response_iteration_{iteration}", None)
|
||||
|
||||
# Emit stats for this iteration
|
||||
self.services.workflow.storeWorkflowStat(
|
||||
self.services.currentWorkflow,
|
||||
response,
|
||||
f"ai.call.{debug_prefix}.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
|
||||
if "[CONTINUE:" in result:
|
||||
# Extract the content before the continuation marker
|
||||
content_part = result.split("[CONTINUE:")[0].strip()
|
||||
if content_part:
|
||||
accumulated_content.append(content_part)
|
||||
logger.info(f"Iteration {iteration}: Continuation detected, continuing...")
|
||||
continue
|
||||
else:
|
||||
# This is the final response
|
||||
accumulated_content.append(result)
|
||||
logger.info(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})")
|
||||
|
||||
# Combine all accumulated content
|
||||
final_result = "\n\n".join(accumulated_content) if accumulated_content else ""
|
||||
|
||||
# Write final result to debug file
|
||||
writeDebugFile(final_result, f"{debug_prefix}_final_result", None)
|
||||
|
||||
logger.info(f"AI call completed: {len(accumulated_content)} parts from {iteration} iterations")
|
||||
return final_result
|
||||
|
||||
def _buildContinuationPrompt(
|
||||
self,
|
||||
base_prompt: str,
|
||||
accumulated_content: List[str],
|
||||
iteration: int
|
||||
) -> str:
|
||||
"""
|
||||
Build a prompt for continuation iterations.
|
||||
"""
|
||||
continuation_instructions = f"""
|
||||
|
||||
CONTINUATION REQUEST (Iteration {iteration}):
|
||||
You are continuing from a previous response. Please continue generating content from where you left off.
|
||||
|
||||
IMPORTANT:
|
||||
- Continue from the exact point where you stopped
|
||||
- Maintain the same format and structure
|
||||
- If you cannot complete the full response, end with: [CONTINUE: brief description of what still needs to be generated]
|
||||
- Only stop when the response is completely generated
|
||||
|
||||
Previous content generated:
|
||||
{chr(10).join(accumulated_content[-1:]) if accumulated_content else "None"}
|
||||
|
||||
Continue generating content now:
|
||||
"""
|
||||
|
||||
return f"{base_prompt}{continuation_instructions}"
|
||||
|
||||
def _buildPromptWithPlaceholders(self, prompt: str, placeholders: Optional[Dict[str, str]]) -> str:
|
||||
"""
|
||||
Build full prompt by replacing placeholders with their content.
|
||||
Uses the new {{KEY:placeholder}} format.
|
||||
"""
|
||||
if not placeholders:
|
||||
return prompt
|
||||
|
||||
full_prompt = prompt
|
||||
for placeholder, content in placeholders.items():
|
||||
# Replace both old format {{placeholder}} and new format {{KEY:placeholder}}
|
||||
full_prompt = full_prompt.replace(f"{{{{{placeholder}}}}}", content)
|
||||
full_prompt = full_prompt.replace(f"{{{{KEY:{placeholder}}}}}", content)
|
||||
|
||||
return full_prompt
|
||||
|
||||
async def _buildGenerationPrompt(
|
||||
self,
|
||||
prompt: str,
|
||||
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,
|
||||
aiService=self,
|
||||
services=self.services
|
||||
)
|
||||
|
||||
# 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,
|
||||
options: Optional[AiCallOptions] = None
|
||||
) -> str:
|
||||
"""
|
||||
Planning AI call for task planning, action planning, action selection, etc.
|
||||
|
||||
Args:
|
||||
prompt: The planning prompt
|
||||
placeholders: Optional list of placeholder replacements
|
||||
options: AI call configuration options
|
||||
|
||||
Returns:
|
||||
Planning JSON response
|
||||
"""
|
||||
if options is None:
|
||||
options = AiCallOptions()
|
||||
|
||||
# Build full prompt with placeholders
|
||||
if placeholders:
|
||||
placeholders_dict = {p.key: p.value for p in placeholders}
|
||||
full_prompt = self._buildPromptWithPlaceholders(prompt, placeholders_dict)
|
||||
else:
|
||||
full_prompt = prompt
|
||||
|
||||
# Use shared core function with planning-specific debug prefix
|
||||
return await self._callAiWithLooping(full_prompt, options, "planning")
|
||||
|
||||
# Document Generation AI Call
|
||||
async def callAiDocuments(
|
||||
self,
|
||||
prompt: str,
|
||||
documents: Optional[List[ChatDocument]] = None,
|
||||
placeholders: Optional[List[PromptPlaceholder]] = None,
|
||||
options: Optional[AiCallOptions] = None,
|
||||
outputFormat: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
|
|
@ -33,94 +232,43 @@ class SubCoreAi:
|
|||
documentGenerator=None
|
||||
) -> Union[str, Dict[str, Any]]:
|
||||
"""
|
||||
Unified AI call interface that automatically routes to appropriate handler.
|
||||
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
|
||||
placeholders: Optional list of placeholder replacements for planning calls
|
||||
options: AI call configuration options
|
||||
outputFormat: Optional output format (html, pdf, docx, txt, md, json, csv, xlsx) for document generation
|
||||
outputFormat: Optional output format for document generation
|
||||
title: Optional title for generated documents
|
||||
documentProcessor: Document processing service instance
|
||||
documentGenerator: Document generation service instance
|
||||
|
||||
Returns:
|
||||
AI response as string, or dict with documents if outputFormat is specified
|
||||
|
||||
Raises:
|
||||
Exception: If all available models fail
|
||||
"""
|
||||
if options is None:
|
||||
options = AiCallOptions()
|
||||
|
||||
# Normalize placeholders from List[PromptPlaceholder]
|
||||
placeholders_dict: Dict[str, str] = {}
|
||||
placeholders_meta: Dict[str, bool] = {}
|
||||
if placeholders:
|
||||
placeholders_dict = {p.label: p.content for p in placeholders}
|
||||
placeholders_meta = {p.label: bool(getattr(p, 'summaryAllowed', False)) for p in placeholders}
|
||||
|
||||
# Auto-determine call type based on documents and operation type
|
||||
call_type = self._determineCallType(documents, options.operationType)
|
||||
options.callType = call_type
|
||||
|
||||
try:
|
||||
# Build the full prompt that will be sent to AI
|
||||
if placeholders:
|
||||
full_prompt = prompt
|
||||
for p in placeholders:
|
||||
placeholder = f"{{{{KEY:{p.label}}}}}"
|
||||
full_prompt = full_prompt.replace(placeholder, p.content)
|
||||
else:
|
||||
full_prompt = prompt
|
||||
|
||||
# Check for unresolved placeholders and clean them up
|
||||
try:
|
||||
import re
|
||||
# Find only {{KEY:...}} patterns that need to be removed
|
||||
unresolved_placeholders = re.findall(r'\{\{KEY:[^}]+\}\}', full_prompt)
|
||||
if unresolved_placeholders:
|
||||
logger.warning(f"Found unresolved KEY placeholders in prompt: {unresolved_placeholders}")
|
||||
# Remove only {{KEY:...}} patterns, leave other {{...}} content intact
|
||||
full_prompt = re.sub(r'\{\{KEY:[^}]+\}\}', '', full_prompt)
|
||||
# Clean up extra whitespace
|
||||
full_prompt = re.sub(r'\n\s*\n\s*\n', '\n\n', full_prompt)
|
||||
full_prompt = full_prompt.strip()
|
||||
logger.info("Cleaned up unresolved KEY placeholders from prompt")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error cleaning up prompt placeholders: {str(e)}")
|
||||
|
||||
# Log the final integrated prompt that AI will receive
|
||||
try:
|
||||
from modules.shared.debugLogger import writeDebugFile
|
||||
# Determine the prompt type based on operation type
|
||||
if options.operationType == OperationType.GENERATE_PLAN:
|
||||
prompt_type = "taskplanPrompt"
|
||||
elif options.operationType == OperationType.ANALYSE_CONTENT:
|
||||
prompt_type = "analysisPrompt"
|
||||
else:
|
||||
prompt_type = "aiPrompt"
|
||||
|
||||
writeDebugFile(full_prompt, prompt_type, documents)
|
||||
except Exception:
|
||||
pass # Don't fail on debug logging
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Handle document generation with specific output format using unified approach
|
||||
if outputFormat and documentGenerator:
|
||||
# Use unified generation method for all document generation
|
||||
if documents and len(documents) > 0:
|
||||
# Extract content from documents first
|
||||
logger.info(f"Extracting content from {len(documents)} documents")
|
||||
extracted_content = await documentProcessor.callAiText(full_prompt, documents, options)
|
||||
# Generate with extracted content
|
||||
generated_json = await self._callAiUnifiedGeneration(full_prompt, extracted_content, options, outputFormat, title)
|
||||
extracted_content = await documentProcessor.callAiText(prompt, documents, options)
|
||||
# Generate with extracted content using shared core function
|
||||
generation_prompt = await self._buildGenerationPrompt(prompt, extracted_content, outputFormat, title)
|
||||
generated_json = await self._callAiWithLooping(generation_prompt, options, "document_generation")
|
||||
else:
|
||||
# Direct generation without documents
|
||||
logger.info("No documents provided - using direct generation")
|
||||
generated_json = await self._callAiUnifiedGeneration(full_prompt, None, options, outputFormat, title)
|
||||
generation_prompt = await self._buildGenerationPrompt(prompt, None, outputFormat, title)
|
||||
generated_json = await self._callAiWithLooping(generation_prompt, options, "document_generation")
|
||||
|
||||
# Write the generated JSON to debug file
|
||||
from modules.shared.debugLogger import writeDebugFile
|
||||
writeDebugFile(generated_json, "unified_generation_response", documents)
|
||||
|
||||
# Parse the generated JSON
|
||||
try:
|
||||
|
|
@ -128,6 +276,13 @@ class SubCoreAi:
|
|||
generated_data = json.loads(generated_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
|
||||
writeDebugFile(generated_json, "failed_json_parsing", None)
|
||||
|
||||
return {"success": False, "error": f"Generated content is not valid JSON: {str(e)}"}
|
||||
|
||||
# Render to final format using the existing renderer
|
||||
|
|
@ -135,7 +290,7 @@ class SubCoreAi:
|
|||
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", full_prompt, self
|
||||
generated_data, outputFormat, title or "Generated Document", prompt, self
|
||||
)
|
||||
|
||||
# Build result in the expected format
|
||||
|
|
@ -162,47 +317,24 @@ class SubCoreAi:
|
|||
writeDebugFile(str(result), "documentGenerationResponse", documents)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error rendering document: {str(e)}")
|
||||
return {"success": False, "error": f"Rendering failed: {str(e)}"}
|
||||
|
||||
if call_type == "planning":
|
||||
result = await self._callAiPlanning(prompt, placeholders_dict, placeholders_meta, options)
|
||||
# Log AI response for debugging
|
||||
try:
|
||||
from modules.shared.debugLogger import writeDebugFile
|
||||
writeDebugFile(str(result or ""), "taskplanResponse", documents)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
# Handle text calls (no output format specified)
|
||||
if documents and documentProcessor:
|
||||
# Use document processing for text calls with documents
|
||||
result = await documentProcessor.callAiText(prompt, documents, options)
|
||||
else:
|
||||
# Set processDocumentsIndividually from the legacy parameter if not set in options
|
||||
if options.processDocumentsIndividually is None and documents:
|
||||
options.processDocumentsIndividually = False # Default to batch processing
|
||||
|
||||
# For text calls, we need to build the full prompt with placeholders here
|
||||
# since _callAiText doesn't handle placeholders directly
|
||||
if placeholders_dict:
|
||||
full_prompt = self._buildPromptWithPlaceholders(prompt, placeholders_dict)
|
||||
else:
|
||||
full_prompt = prompt
|
||||
|
||||
if documentProcessor and documents:
|
||||
result = await documentProcessor.callAiText(full_prompt, documents, options)
|
||||
else:
|
||||
# Enhanced direct AI call with partial results support
|
||||
result = await self._callAiWithPartialResults(full_prompt, options)
|
||||
|
||||
# Log AI response for debugging (additional logging for text calls)
|
||||
try:
|
||||
from modules.shared.debugLogger import writeDebugFile
|
||||
writeDebugFile(str(result or ""), "aiTextResponse", documents)
|
||||
except Exception:
|
||||
pass
|
||||
# Use shared core function for direct text calls
|
||||
result = await self._callAiWithLooping(prompt, options, "text")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# AI Image Analysis
|
||||
async def readImage(
|
||||
self,
|
||||
|
|
@ -312,382 +444,14 @@ class SubCoreAi:
|
|||
else:
|
||||
return "text"
|
||||
|
||||
async def _callAiPlanning(
|
||||
self,
|
||||
prompt: str,
|
||||
placeholders: Optional[Dict[str, str]],
|
||||
placeholdersMeta: Optional[Dict[str, bool]],
|
||||
options: AiCallOptions
|
||||
) -> str:
|
||||
"""
|
||||
Handle planning calls with placeholder system and selective summarization.
|
||||
"""
|
||||
# Build full prompt with placeholders; if too large, summarize summaryAllowed placeholders proportionally
|
||||
effective_placeholders = placeholders or {}
|
||||
full_prompt = self._buildPromptWithPlaceholders(prompt, effective_placeholders)
|
||||
|
||||
if options.compressPrompt and placeholdersMeta:
|
||||
# Determine model capacity
|
||||
try:
|
||||
caps = self._getModelCapabilitiesForContent(full_prompt, None, options)
|
||||
max_bytes = caps.get("maxContextBytes", len(full_prompt.encode("utf-8")))
|
||||
except Exception:
|
||||
max_bytes = len(full_prompt.encode("utf-8"))
|
||||
|
||||
current_bytes = len(full_prompt.encode("utf-8"))
|
||||
if current_bytes > max_bytes:
|
||||
# Compute total bytes contributed by allowed placeholders (approximate by content length)
|
||||
allowed_labels = [l for l, allow in placeholdersMeta.items() if allow]
|
||||
allowed_sizes = {l: len((effective_placeholders.get(l) or "").encode("utf-8")) for l in allowed_labels}
|
||||
total_allowed = sum(allowed_sizes.values())
|
||||
|
||||
overage = current_bytes - max_bytes
|
||||
if total_allowed > 0 and overage > 0:
|
||||
# Target total for allowed after reduction
|
||||
target_allowed = max(total_allowed - overage, 0)
|
||||
# Global ratio to apply across allowed placeholders
|
||||
ratio = target_allowed / total_allowed if total_allowed > 0 else 1.0
|
||||
ratio = max(0.0, min(1.0, ratio))
|
||||
|
||||
reduced: Dict[str, str] = {}
|
||||
for label, content in effective_placeholders.items():
|
||||
if label in allowed_labels and isinstance(content, str) and len(content) > 0:
|
||||
old_len = len(content)
|
||||
# Reduce by proportional ratio on characters (fallback if empty)
|
||||
reduction_factor = ratio if old_len > 0 else 1.0
|
||||
reduced[label] = self._reduceText(content, reduction_factor)
|
||||
else:
|
||||
reduced[label] = content
|
||||
|
||||
effective_placeholders = reduced
|
||||
full_prompt = self._buildPromptWithPlaceholders(prompt, effective_placeholders)
|
||||
|
||||
# If still slightly over, perform a second-pass fine adjustment with updated ratio
|
||||
current_bytes = len(full_prompt.encode("utf-8"))
|
||||
if current_bytes > max_bytes and total_allowed > 0:
|
||||
overage2 = current_bytes - max_bytes
|
||||
# Recompute allowed sizes after first reduction
|
||||
allowed_sizes2 = {l: len((effective_placeholders.get(l) or "").encode("utf-8")) for l in allowed_labels}
|
||||
total_allowed2 = sum(allowed_sizes2.values())
|
||||
if total_allowed2 > 0 and overage2 > 0:
|
||||
target_allowed2 = max(total_allowed2 - overage2, 0)
|
||||
ratio2 = target_allowed2 / total_allowed2
|
||||
ratio2 = max(0.0, min(1.0, ratio2))
|
||||
reduced2: Dict[str, str] = {}
|
||||
for label, content in effective_placeholders.items():
|
||||
if label in allowed_labels and isinstance(content, str) and len(content) > 0:
|
||||
old_len = len(content)
|
||||
reduction_factor = ratio2 if old_len > 0 else 1.0
|
||||
reduced2[label] = self._reduceText(content, reduction_factor)
|
||||
else:
|
||||
reduced2[label] = content
|
||||
effective_placeholders = reduced2
|
||||
full_prompt = self._buildPromptWithPlaceholders(prompt, effective_placeholders)
|
||||
|
||||
|
||||
# Make AI call using AiObjects (let it handle model selection)
|
||||
request = AiCallRequest(
|
||||
prompt=full_prompt,
|
||||
context="", # Context is already included in the prompt
|
||||
options=options
|
||||
)
|
||||
response = await self.aiObjects.call(request)
|
||||
try:
|
||||
logger.debug(f"AI model selected (planning): {getattr(response, 'modelName', 'unknown')}")
|
||||
except Exception:
|
||||
pass
|
||||
return response.content
|
||||
|
||||
async def _callAiWithPartialResults(
|
||||
self,
|
||||
prompt: str,
|
||||
options: AiCallOptions
|
||||
) -> str:
|
||||
"""
|
||||
Call AI with partial results continuation logic for direct calls.
|
||||
Handles cases where AI needs to generate large responses in chunks.
|
||||
"""
|
||||
logger.info("Starting direct AI call with partial results support")
|
||||
|
||||
# Build enhanced prompt with continuation instructions
|
||||
enhanced_prompt = self._buildDirectContinuationPrompt(prompt)
|
||||
|
||||
# Process with continuation logic
|
||||
return await self._processDirectWithContinuationLoop(enhanced_prompt, options)
|
||||
|
||||
def _buildDirectContinuationPrompt(self, base_prompt: str) -> str:
|
||||
"""
|
||||
Build a prompt for direct AI calls that includes partial results instructions.
|
||||
"""
|
||||
continuation_instructions = """
|
||||
|
||||
IMPORTANT: If your response is too large to generate completely in one response, you can deliver partial results and continue.
|
||||
|
||||
CONTINUATION LOGIC:
|
||||
- If you cannot complete the full response, end your response with:
|
||||
[CONTINUE: brief description of what still needs to be generated]
|
||||
- The system will call you again to continue from where you left off
|
||||
- Continue generating from the exact point where you stopped
|
||||
- Maintain consistency with your previous partial response
|
||||
- Only stop when you have generated the complete response
|
||||
|
||||
Examples:
|
||||
|
||||
Example - Code Generation:
|
||||
If generating a large code file and you can only generate part of it:
|
||||
- Generate the first part (imports, classes, functions)
|
||||
- End with: [CONTINUE: Generate the remaining methods and main execution code]
|
||||
- In the next call, continue from where you left off
|
||||
|
||||
Example - Documentation:
|
||||
If writing comprehensive documentation and you can only generate sections 1-3:
|
||||
- Generate sections 1-3 with full content
|
||||
- End with: [CONTINUE: Generate sections 4-8 covering advanced topics and examples]
|
||||
- In the next call, continue with sections 4-8
|
||||
|
||||
This allows you to handle very large responses that exceed normal limits.
|
||||
"""
|
||||
|
||||
return f"{base_prompt}{continuation_instructions}"
|
||||
|
||||
async def _processDirectWithContinuationLoop(
|
||||
self,
|
||||
enhanced_prompt: str,
|
||||
options: AiCallOptions
|
||||
) -> str:
|
||||
"""
|
||||
Process direct AI call with continuation loop until complete.
|
||||
"""
|
||||
max_iterations = 10 # Prevent infinite loops
|
||||
iteration = 0
|
||||
accumulated_content = []
|
||||
continuation_hint = None
|
||||
|
||||
while iteration < max_iterations:
|
||||
iteration += 1
|
||||
logger.info(f"Direct AI continuation iteration {iteration}/{max_iterations}")
|
||||
|
||||
# Build prompt for this iteration
|
||||
if continuation_hint:
|
||||
iteration_prompt = self._buildDirectContinuationIterationPrompt(
|
||||
enhanced_prompt, continuation_hint, accumulated_content
|
||||
)
|
||||
else:
|
||||
iteration_prompt = enhanced_prompt
|
||||
|
||||
# Make AI call for this iteration
|
||||
try:
|
||||
request = AiCallRequest(
|
||||
prompt=iteration_prompt,
|
||||
context="",
|
||||
options=options
|
||||
)
|
||||
response = await self.aiObjects.call(request)
|
||||
result = response.content
|
||||
|
||||
# Emit stats for this iteration
|
||||
self.services.workflow.storeWorkflowStat(
|
||||
self.services.currentWorkflow,
|
||||
response,
|
||||
f"ai.call.{options.operationType}.iteration_{iteration}"
|
||||
)
|
||||
|
||||
if not result or not result.strip():
|
||||
logger.warning(f"Iteration {iteration}: Empty response, stopping")
|
||||
break
|
||||
|
||||
# Check for continuation marker
|
||||
if "[CONTINUE:" in result:
|
||||
# Extract the continuation hint
|
||||
import re
|
||||
continue_match = re.search(r'\[CONTINUE:\s*([^\]]+)\]', result)
|
||||
if continue_match:
|
||||
continuation_hint = continue_match.group(1).strip()
|
||||
# Remove the continuation marker from the result
|
||||
result = re.sub(r'\s*\[CONTINUE:[^\]]+\]', '', result).strip()
|
||||
else:
|
||||
continuation_hint = "Continue from where you left off"
|
||||
|
||||
# Add this partial result to accumulated content
|
||||
if result.strip():
|
||||
accumulated_content.append(result.strip())
|
||||
|
||||
logger.info(f"Iteration {iteration}: Partial result added, continue hint: {continuation_hint}")
|
||||
else:
|
||||
# No continuation marker - this is the final result
|
||||
if result.strip():
|
||||
accumulated_content.append(result.strip())
|
||||
|
||||
logger.info(f"Direct AI continuation complete after {iteration} iterations")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Direct AI iteration {iteration} failed: {str(e)}")
|
||||
break
|
||||
|
||||
if iteration >= max_iterations:
|
||||
logger.warning(f"Direct AI continuation stopped after maximum iterations ({max_iterations})")
|
||||
|
||||
# For JSON responses, we need to merge them properly instead of concatenating
|
||||
if accumulated_content:
|
||||
import json
|
||||
# Parse each part as JSON and merge them
|
||||
merged_documents = []
|
||||
merged_metadata = None
|
||||
|
||||
for content in accumulated_content:
|
||||
parsed = json.loads(content)
|
||||
if isinstance(parsed, dict):
|
||||
# Extract metadata from first valid JSON
|
||||
if merged_metadata is None and "metadata" in parsed:
|
||||
merged_metadata = parsed["metadata"]
|
||||
|
||||
# Extract documents from this part
|
||||
if "documents" in parsed and isinstance(parsed["documents"], list):
|
||||
merged_documents.extend(parsed["documents"])
|
||||
|
||||
# Create final merged JSON - NO FALLBACK
|
||||
final_result = json.dumps({
|
||||
"metadata": merged_metadata or {
|
||||
"title": "Generated Document",
|
||||
"splitStrategy": "single_document",
|
||||
"source_documents": [],
|
||||
"extraction_method": "ai_generation"
|
||||
},
|
||||
"documents": merged_documents
|
||||
}, indent=2)
|
||||
else:
|
||||
# Return empty JSON structure if no content
|
||||
final_result = json.dumps({
|
||||
"metadata": {
|
||||
"title": "Generated Document",
|
||||
"splitStrategy": "single_document",
|
||||
"source_documents": [],
|
||||
"extraction_method": "ai_generation"
|
||||
},
|
||||
"documents": []
|
||||
}, indent=2)
|
||||
|
||||
logger.info(f"Final direct AI result: {len(accumulated_content)} parts from {iteration} iterations")
|
||||
return final_result
|
||||
|
||||
def _buildDirectContinuationIterationPrompt(
|
||||
self,
|
||||
base_prompt: str,
|
||||
continuation_hint: str,
|
||||
accumulated_content: List[str]
|
||||
) -> str:
|
||||
"""
|
||||
Build a prompt for continuation iteration with context.
|
||||
"""
|
||||
# Build context of what's already been generated
|
||||
context_summary = "PREVIOUSLY GENERATED CONTENT:\n"
|
||||
for i, content in enumerate(accumulated_content[-2:]): # Show last 2 parts for context
|
||||
preview = content[:200] + "..." if len(content) > 200 else content
|
||||
context_summary += f"Part {i+1}: {preview}\n"
|
||||
|
||||
continuation_prompt = f"""
|
||||
{base_prompt}
|
||||
|
||||
{context_summary}
|
||||
|
||||
CONTINUATION INSTRUCTIONS:
|
||||
- Continue from where you left off
|
||||
- Continuation hint: {continuation_hint}
|
||||
- Generate the next part of the content
|
||||
- Maintain consistency with previously generated content
|
||||
- End with [CONTINUE: description] if more content is needed
|
||||
- End without [CONTINUE] if the response is complete
|
||||
"""
|
||||
|
||||
return continuation_prompt
|
||||
|
||||
async def _callAiUnifiedGeneration(
|
||||
self,
|
||||
prompt: str,
|
||||
extracted_content: Optional[str] = None,
|
||||
options: Optional[AiCallOptions] = None,
|
||||
outputFormat: str = "json",
|
||||
title: str = "Generated Document"
|
||||
) -> str:
|
||||
"""
|
||||
Unified generation method that handles both scenarios:
|
||||
- With extracted content (from documents)
|
||||
- Without extracted content (direct generation)
|
||||
|
||||
Always uses continuation logic for long responses.
|
||||
Always returns standardized JSON format using the multi-document schema.
|
||||
"""
|
||||
if options is None:
|
||||
options = AiCallOptions()
|
||||
|
||||
logger.info("Starting unified AI generation with continuation logic")
|
||||
|
||||
# Use the existing buildGenerationPrompt to get the proper canonical format instructions
|
||||
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,
|
||||
aiService=self,
|
||||
services=self.services
|
||||
)
|
||||
|
||||
# If we have extracted content, prepend it to the prompt
|
||||
if extracted_content:
|
||||
generation_prompt = f"""EXTRACTED CONTENT FROM DOCUMENTS:
|
||||
{extracted_content}
|
||||
|
||||
{generation_prompt}"""
|
||||
|
||||
# Use continuation logic for long responses
|
||||
return await self._processDirectWithContinuationLoop(generation_prompt, options)
|
||||
|
||||
async def _callAiDirect(
|
||||
self,
|
||||
prompt: str,
|
||||
documents: Optional[List[ChatDocument]],
|
||||
options: AiCallOptions,
|
||||
documentProcessor=None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Call AI directly with prompt and documents for JSON output.
|
||||
Used for multi-file generation - uses the existing generation pipeline.
|
||||
"""
|
||||
# Use the existing generation pipeline that already works
|
||||
# This ensures proper document processing and content extraction
|
||||
logger.info(f"Using existing generation pipeline for {len(documents) if documents else 0} documents")
|
||||
|
||||
if documentProcessor:
|
||||
# Process documents with JSON merging using the existing pipeline
|
||||
result = await documentProcessor.processDocumentsPerChunkJson(documents, prompt, options)
|
||||
else:
|
||||
# Fallback to simple AI call
|
||||
request = AiCallRequest(
|
||||
prompt=prompt,
|
||||
context="",
|
||||
options=options
|
||||
)
|
||||
response = await self.aiObjects.call(request)
|
||||
result = {"metadata": {"title": "AI Response"}, "sections": [{"id": "section_1", "content_type": "paragraph", "elements": [{"text": response.content}]}]}
|
||||
|
||||
# Convert single-file result to multi-file format if needed
|
||||
if "sections" in result and "documents" not in result:
|
||||
logger.info("Converting single-file result to multi-file format")
|
||||
# This is a single-file result, convert it to multi-file format
|
||||
return {
|
||||
"metadata": result.get("metadata", {"title": "Converted Document"}),
|
||||
"documents": [{
|
||||
"id": "doc_1",
|
||||
"title": result.get("metadata", {}).get("title", "Document"),
|
||||
"filename": "document.txt",
|
||||
"sections": result.get("sections", [])
|
||||
}]
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def _getModelCapabilitiesForContent(self, prompt: str, documents: Optional[List[ChatDocument]], options: AiCallOptions) -> Dict[str, int]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@ class SubDocumentGeneration:
|
|||
|
||||
# Update progress - generating extraction prompt
|
||||
progressLogger.updateProgress(operationId, 0.1, "Generating prompt")
|
||||
|
||||
# Write prompt to debug file
|
||||
from modules.shared.debugLogger import writeDebugFile
|
||||
writeDebugFile(extractionPrompt, "extraction_prompt", documents)
|
||||
|
||||
# Process with unified JSON pipeline using continuation logic
|
||||
aiResponse = await self.documentProcessor.processDocumentsWithContinuation(
|
||||
|
|
@ -109,11 +113,13 @@ class SubDocumentGeneration:
|
|||
# Update progress - AI processing completed
|
||||
progressLogger.updateProgress(operationId, 0.6, "Processing done")
|
||||
|
||||
# Log the AI response for debugging
|
||||
logger.info(f"AI response received for validation:")
|
||||
logger.info(f" - Type: {type(aiResponse)}")
|
||||
logger.info(f" - Keys: {list(aiResponse.keys()) if isinstance(aiResponse, dict) else 'Not a dict'}")
|
||||
logger.info(f" - Content: {aiResponse}")
|
||||
|
||||
|
||||
# Write AI response to debug file
|
||||
from modules.shared.debugLogger import writeDebugFile
|
||||
import json
|
||||
response_json = json.dumps(aiResponse, indent=2, ensure_ascii=False) if isinstance(aiResponse, dict) else str(aiResponse)
|
||||
writeDebugFile(response_json, "ai_response", documents)
|
||||
|
||||
# Validate response structure
|
||||
if not self._validateUnifiedResponseStructure(aiResponse):
|
||||
|
|
|
|||
|
|
@ -605,7 +605,13 @@ CONTINUATION INSTRUCTIONS:
|
|||
import os
|
||||
from datetime import datetime, UTC
|
||||
ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
||||
debug_root = "./test-chat/ai"
|
||||
# Use configured log directory instead of hardcoded test-chat
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./")
|
||||
if not os.path.isabs(logDir):
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
logDir = os.path.join(gatewayDir, logDir)
|
||||
debug_root = os.path.join(logDir, 'debug')
|
||||
os.makedirs(debug_root, exist_ok=True)
|
||||
with open(os.path.join(debug_root, f"{ts}_extraction_image_chunk_{chunk_index}.txt"), "w", encoding="utf-8") as f:
|
||||
f.write(f"EXTRACTION IMAGE RESPONSE:\n{ai_result if ai_result else 'No response'}\n")
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class SubUtilities:
|
|||
pass
|
||||
|
||||
def _writeAiResponseDebug(self, label: str, content: str, partIndex: int = 1, modelName: str = None, continuation: bool = None) -> None:
|
||||
"""Persist raw AI response parts for debugging under test-chat/ai - only if debug enabled."""
|
||||
"""Persist raw AI response parts for debugging under configured log directory - only if debug enabled."""
|
||||
try:
|
||||
# Check if debug logging is enabled
|
||||
debug_enabled = self.services.utils.configGet("APP_DEBUG_CHAT_WORKFLOW_ENABLED", False)
|
||||
|
|
@ -70,10 +70,13 @@ class SubUtilities:
|
|||
|
||||
import os
|
||||
from datetime import datetime, UTC
|
||||
# Base dir: gateway/test-chat/ai (go up 4 levels from this file)
|
||||
# .../gateway/modules/services/serviceAi/subUtilities.py -> up to gateway root
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
outDir = os.path.join(gatewayDir, 'test-chat', 'ai')
|
||||
# Use configured log directory instead of hardcoded test-chat
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./")
|
||||
if not os.path.isabs(logDir):
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
logDir = os.path.join(gatewayDir, logDir)
|
||||
outDir = os.path.join(logDir, 'debug')
|
||||
os.makedirs(outDir, exist_ok=True)
|
||||
ts = datetime.now(UTC).strftime('%Y%m%d-%H%M%S-%f')[:-3]
|
||||
suffix = []
|
||||
|
|
|
|||
|
|
@ -403,7 +403,13 @@ DO NOT return a schema description - return actual extracted content in the JSON
|
|||
import os
|
||||
from datetime import datetime, UTC
|
||||
ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
||||
debug_root = "./test-chat/ai"
|
||||
# Use configured log directory instead of hardcoded test-chat
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./")
|
||||
if not os.path.isabs(logDir):
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
logDir = os.path.join(gatewayDir, logDir)
|
||||
debug_root = os.path.join(logDir, 'debug')
|
||||
os.makedirs(debug_root, exist_ok=True)
|
||||
with open(os.path.join(debug_root, f"{ts}_extraction_prompt.txt"), "w", encoding="utf-8") as f:
|
||||
f.write(finalPrompt)
|
||||
|
|
@ -435,118 +441,70 @@ async def buildGenerationPrompt(
|
|||
# Debug output
|
||||
services.utils.debugLogToFile(f"GENERATION PROMPT REQUEST: buildGenerationPrompt called with outputFormat='{outputFormat}', title='{title}'", "PROMPT_BUILDER")
|
||||
|
||||
# AI call to generate the appropriate generation prompt
|
||||
generationPromptRequest = f"""
|
||||
You are creating instructions for an AI to generate JSON content in the CANONICAL FORMAT that will be converted to a {outputFormat} document.
|
||||
# Return static generation prompt template instead of calling AI
|
||||
services.utils.debugLogToFile("GENERATION PROMPT REQUEST: Using static template instead of AI call", "PROMPT_BUILDER")
|
||||
|
||||
# Return static generation prompt template
|
||||
result = f"""You are an AI assistant that generates structured JSON content for document creation.
|
||||
|
||||
User request: "{safeUserPrompt}"
|
||||
Document title: "{title}"
|
||||
Target format: {outputFormat}
|
||||
USER REQUEST: "{safeUserPrompt}"
|
||||
DOCUMENT TITLE: "{title}"
|
||||
TARGET FORMAT: {outputFormat}
|
||||
|
||||
Write clear, detailed instructions that tell the AI how to generate JSON content using the CANONICAL JSON FORMAT. Focus on:
|
||||
TASK: Generate JSON content that fulfills the user's request.
|
||||
|
||||
1. What content is most important for the user
|
||||
2. How to structure and organize the content using the canonical JSON format with 'sections'
|
||||
3. Specific formatting requirements for the target format
|
||||
4. Language requirements to preserve
|
||||
5. How to ensure the JSON content meets the user's needs
|
||||
|
||||
CRITICAL: The AI MUST generate content using the CANONICAL JSON FORMAT with this exact structure:
|
||||
CRITICAL: You MUST return ONLY valid JSON in this exact structure:
|
||||
{{
|
||||
"metadata": {{
|
||||
"title": "Document Title"
|
||||
"title": "{title}",
|
||||
"splitStrategy": "single_document",
|
||||
"source_documents": [],
|
||||
"extraction_method": "ai_generation"
|
||||
}},
|
||||
"sections": [
|
||||
"documents": [
|
||||
{{
|
||||
"id": "section_1",
|
||||
"content_type": "heading",
|
||||
"elements": [
|
||||
"id": "doc_1",
|
||||
"title": "{title}",
|
||||
"filename": "document.{outputFormat}",
|
||||
"sections": [
|
||||
{{
|
||||
"level": 1,
|
||||
"text": "1. SECTION TITLE"
|
||||
}}
|
||||
],
|
||||
"order": 1
|
||||
}},
|
||||
{{
|
||||
"id": "section_2",
|
||||
"content_type": "paragraph",
|
||||
"elements": [
|
||||
"id": "section_1",
|
||||
"content_type": "heading",
|
||||
"elements": [
|
||||
{{
|
||||
"level": 1,
|
||||
"text": "1. SECTION TITLE"
|
||||
}}
|
||||
],
|
||||
"order": 1
|
||||
}},
|
||||
{{
|
||||
"text": "This is the actual content that should be extracted from the document."
|
||||
"id": "section_2",
|
||||
"content_type": "paragraph",
|
||||
"elements": [
|
||||
{{
|
||||
"text": "This is the actual content that should be generated."
|
||||
}}
|
||||
],
|
||||
"order": 2
|
||||
}}
|
||||
],
|
||||
"order": 2
|
||||
}},
|
||||
{{
|
||||
"id": "section_3",
|
||||
"content_type": "table",
|
||||
"elements": [
|
||||
{{
|
||||
"headers": ["Column 1", "Column 2", "Column 3"],
|
||||
"rows": [
|
||||
["Value 1", "Value 2", "Value 3"],
|
||||
["Value 4", "Value 5", "Value 6"]
|
||||
]
|
||||
}}
|
||||
],
|
||||
"order": 3
|
||||
]
|
||||
}}
|
||||
],
|
||||
"continue": false
|
||||
]
|
||||
}}
|
||||
|
||||
IMPORTANT CHUNKING LOGIC:
|
||||
- If the document is too large to generate completely in one response, set "continue": true
|
||||
- When "continue": true, include a "continuation_context" field with:
|
||||
- "last_section_id": "id of the last completed section"
|
||||
- "last_element_index": "index of the last completed element in that section"
|
||||
- "remaining_requirements": "brief description of what still needs to be generated"
|
||||
- The AI will be called again with this context to continue generation
|
||||
- Only set "continue": false when the document is completely generated
|
||||
|
||||
The AI should NOT create format-specific structures like "sheets" or "columns" - only use the canonical format with "sections" and "elements".
|
||||
|
||||
Write the instructions as plain text, not JSON. Start with "Generate JSON content that..." and provide clear, actionable instructions for creating structured JSON data in the canonical format.
|
||||
IMPORTANT:
|
||||
- Return ONLY the JSON structure above
|
||||
- Do NOT include any text before or after the JSON
|
||||
- Fill in the actual content based on the user request: {safeUserPrompt}
|
||||
- If the content is too large, you can split it into multiple sections
|
||||
- Each section should have a unique id and appropriate content_type
|
||||
"""
|
||||
|
||||
# Call AI service to generate the prompt
|
||||
services.utils.debugLogToFile("GENERATION PROMPT REQUEST: Calling AI for generation prompt...", "PROMPT_BUILDER")
|
||||
|
||||
# Import and set proper options for AI call
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType
|
||||
request_options = AiCallOptions()
|
||||
request_options.operationType = OperationType.GENERAL
|
||||
|
||||
request = AiCallRequest(prompt=generationPromptRequest, context="", options=request_options)
|
||||
response = await aiService.aiObjects.call(request)
|
||||
result = response.content if response else ""
|
||||
|
||||
# Replace the placeholder that the AI created with actual format rules
|
||||
if result:
|
||||
formatRules = _getFormatRules(outputFormat)
|
||||
result = result.replace("PLACEHOLDER_FOR_FORMAT_RULES", formatRules)
|
||||
|
||||
# Debug output
|
||||
services.utils.debugLogToFile(f"GENERATION PROMPT: Generated successfully", "PROMPT_BUILDER")
|
||||
|
||||
# Save full generation prompt and AI response to debug file - only if debug enabled
|
||||
try:
|
||||
debug_enabled = services.utils.configGet("APP_DEBUG_CHAT_WORKFLOW_ENABLED", False)
|
||||
if debug_enabled:
|
||||
import os
|
||||
from datetime import datetime, UTC
|
||||
ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
||||
debug_root = "./test-chat/ai"
|
||||
os.makedirs(debug_root, exist_ok=True)
|
||||
with open(os.path.join(debug_root, f"{ts}_generation_prompt.txt"), "w", encoding="utf-8") as f:
|
||||
f.write(f"GENERATION PROMPT REQUEST:\n{generationPromptRequest}\n\n")
|
||||
f.write(f"GENERATION PROMPT AI RESPONSE:\n{response.content if response else 'No response'}\n\n")
|
||||
f.write(f"GENERATION PROMPT FINAL:\n{result if result else 'None'}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result if result else f"Generate a comprehensive {outputFormat} document titled '{title}' based on the extracted content."
|
||||
return result.strip()
|
||||
|
||||
except Exception as e:
|
||||
# Fallback on any error - preserve user prompt for language instructions
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ class NormalizationService:
|
|||
" \"Date\": {\"formats\": [\"DD.MM.YYYY\",\"YYYY-MM-DD\"]}\n }\n}\n"
|
||||
)
|
||||
|
||||
response = await self.services.ai.callAi(prompt=prompt)
|
||||
response = await self.services.ai.coreAi.callAiPlanning(prompt=prompt, placeholders=None, options=None)
|
||||
if not response:
|
||||
return {"mapping": {}, "normalizationPolicy": {}}
|
||||
|
||||
|
|
@ -244,7 +244,13 @@ class NormalizationService:
|
|||
debugEnabled = self.services.utils.configGet("APP_DEBUG_CHAT_WORKFLOW_ENABLED", False)
|
||||
if not debugEnabled:
|
||||
return
|
||||
root = "./test-chat/ai"
|
||||
# Use configured log directory instead of hardcoded test-chat
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./")
|
||||
if not os.path.isabs(logDir):
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
logDir = os.path.join(gatewayDir, logDir)
|
||||
root = os.path.join(logDir, 'debug')
|
||||
os.makedirs(root, exist_ok=True)
|
||||
# Prefix timestamp for files that are frequently overwritten
|
||||
ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
||||
|
|
|
|||
|
|
@ -157,7 +157,12 @@ class UtilsService:
|
|||
return
|
||||
|
||||
# Get debug directory
|
||||
debug_dir = self.configGet("APP_DEBUG_CHAT_WORKFLOW_DIR", "./test-chat")
|
||||
# Use configured log directory instead of hardcoded test-chat
|
||||
logDir = self.configGet("APP_LOGGING_LOG_DIR", "./")
|
||||
if not os.path.isabs(logDir):
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
logDir = os.path.join(gatewayDir, logDir)
|
||||
debug_dir = os.path.join(logDir, 'debug')
|
||||
if not os.path.isabs(debug_dir):
|
||||
# If relative path, make it relative to the gateway directory
|
||||
gateway_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class WorkflowService:
|
|||
|
||||
# Get summary using AI service directly (avoiding circular dependency)
|
||||
ai_service = AiService(self)
|
||||
return await ai_service.callAi(
|
||||
return await ai_service.coreAi.callAiDocuments(
|
||||
prompt=prompt,
|
||||
documents=None,
|
||||
options={
|
||||
|
|
@ -69,7 +69,9 @@ class WorkflowService:
|
|||
"compress_prompt": True,
|
||||
"compress_documents": False,
|
||||
"max_cost": 0.01
|
||||
}
|
||||
},
|
||||
documentProcessor=ai_service.documentProcessor,
|
||||
documentGenerator=ai_service.documentGenerator
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -1,16 +1,25 @@
|
|||
"""
|
||||
Simple debug logger for AI prompts and responses.
|
||||
Writes files chronologically to gateway/test-chat/ai/ with sequential numbering.
|
||||
Writes files chronologically to the configured log directory with sequential numbering.
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime, UTC
|
||||
from typing import List, Optional
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
|
||||
|
||||
def _getDebugDir() -> str:
|
||||
"""Get the debug directory path."""
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
return os.path.join(gatewayDir, 'test-chat', 'ai')
|
||||
"""Get the debug directory path from configuration."""
|
||||
# Get log directory from config (same as used by main logging system)
|
||||
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./")
|
||||
if not os.path.isabs(logDir):
|
||||
# If relative path, make it relative to the gateway directory
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
logDir = os.path.join(gatewayDir, logDir)
|
||||
|
||||
# Create debug subdirectory within the log directory
|
||||
debugDir = os.path.join(logDir, 'debug')
|
||||
return debugDir
|
||||
|
||||
|
||||
def _getNextSequenceNumber() -> int:
|
||||
|
|
|
|||
|
|
@ -106,23 +106,8 @@ class MethodAi(MethodBase):
|
|||
if chatDocuments:
|
||||
logger.info(f"Prepared {len(chatDocuments)} documents for AI processing")
|
||||
|
||||
# Update progress - building prompt
|
||||
progressLogger.updateProgress(operationId, 0.4, "Building prompt")
|
||||
|
||||
# Build enhanced prompt
|
||||
enhanced_prompt = aiPrompt
|
||||
|
||||
# Add processing mode instructions if specified (generic, not analysis-specific)
|
||||
if processingMode == "detailed":
|
||||
enhanced_prompt += "\n\nPlease provide a detailed response with comprehensive information."
|
||||
elif processingMode == "advanced":
|
||||
enhanced_prompt += "\n\nPlease provide an advanced response with deep insights."
|
||||
|
||||
# Note: customInstructions parameter was removed as it's not defined in the method signature
|
||||
|
||||
# Add format guidance to prompt
|
||||
if normalized_result_type != "txt":
|
||||
enhanced_prompt += f"\n\nPlease deliver the result in {normalized_result_type.upper()} format. Ensure the output follows the proper {normalized_result_type.upper()} syntax and structure."
|
||||
# Update progress - preparing AI call
|
||||
progressLogger.updateProgress(operationId, 0.4, "Preparing AI call")
|
||||
|
||||
# Build options and delegate document handling to AI/Extraction/Generation services
|
||||
output_format = output_extension.replace('.', '') or 'txt'
|
||||
|
|
@ -139,17 +124,16 @@ class MethodAi(MethodBase):
|
|||
requiredTags=requiredTags
|
||||
)
|
||||
|
||||
supported_generation_formats = {"html", "pdf", "docx", "txt", "md", "json", "csv", "xlsx"}
|
||||
output_format_arg = output_format if output_format in supported_generation_formats else None
|
||||
|
||||
# Update progress - calling AI
|
||||
progressLogger.updateProgress(operationId, 0.6, "Calling AI")
|
||||
|
||||
result = await self.services.ai.callAi(
|
||||
prompt=enhanced_prompt,
|
||||
result = await self.services.ai.coreAi.callAiDocuments(
|
||||
prompt=aiPrompt, # Use original prompt, let unified generation handle prompt building
|
||||
documents=chatDocuments if chatDocuments else None,
|
||||
options=options,
|
||||
outputFormat=output_format_arg
|
||||
outputFormat=output_format,
|
||||
documentProcessor=self.services.ai.documentProcessor,
|
||||
documentGenerator=self.services.ai.documentGenerator
|
||||
)
|
||||
|
||||
# Update progress - processing result
|
||||
|
|
|
|||
|
|
@ -1186,7 +1186,7 @@ Return JSON:
|
|||
|
||||
# Call AI service to generate email content
|
||||
try:
|
||||
ai_response = await self.services.ai.callAi(
|
||||
ai_response = await self.services.ai.coreAi.callAiDocuments(
|
||||
prompt=ai_prompt,
|
||||
documents=chatDocuments,
|
||||
options=AiCallOptions(
|
||||
|
|
@ -1199,7 +1199,9 @@ Return JSON:
|
|||
resultFormat="json",
|
||||
maxCost=0.50,
|
||||
maxProcessingTime=30
|
||||
)
|
||||
),
|
||||
documentProcessor=self.services.ai.documentProcessor,
|
||||
documentGenerator=self.services.ai.documentGenerator
|
||||
)
|
||||
|
||||
# Parse AI response
|
||||
|
|
|
|||
|
|
@ -120,9 +120,9 @@ DELIVERED CONTENT TO CHECK:
|
|||
request_options = AiCallOptions()
|
||||
request_options.operationType = OperationType.GENERAL
|
||||
|
||||
response = await self.services.ai.callAi(
|
||||
response = await self.services.ai.coreAi.callAiPlanning(
|
||||
prompt=validationPrompt,
|
||||
documents=None,
|
||||
placeholders=None,
|
||||
options=request_options
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -63,9 +63,9 @@ CRITICAL: Respond with ONLY the JSON object below. Do not include any explanator
|
|||
request_options = AiCallOptions()
|
||||
request_options.operationType = OperationType.GENERAL
|
||||
|
||||
response = await self.services.ai.callAi(
|
||||
response = await self.services.ai.coreAi.callAiPlanning(
|
||||
prompt=analysisPrompt,
|
||||
documents=None,
|
||||
placeholders=None,
|
||||
options=request_options
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class TaskPlanner:
|
|||
maxProcessingTime=30
|
||||
)
|
||||
|
||||
prompt = await self.services.ai.callAi(
|
||||
prompt = await self.services.ai.coreAi.callAiPlanning(
|
||||
prompt=taskPlanningPromptTemplate,
|
||||
placeholders=placeholders,
|
||||
options=options
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class ActionplanMode(BaseMode):
|
|||
maxProcessingTime=30
|
||||
)
|
||||
|
||||
prompt = await self.services.ai.callAi(prompt=actionPromptTemplate, placeholders=placeholders, options=options)
|
||||
prompt = await self.services.ai.coreAi.callAiPlanning(prompt=actionPromptTemplate, placeholders=placeholders, options=options)
|
||||
|
||||
# Check if AI response is valid
|
||||
if not prompt:
|
||||
|
|
@ -476,7 +476,7 @@ class ActionplanMode(BaseMode):
|
|||
maxProcessingTime=30
|
||||
)
|
||||
|
||||
response = await self.services.ai.callAi(prompt=promptTemplate, placeholders=placeholders, options=options)
|
||||
response = await self.services.ai.coreAi.callAiPlanning(prompt=promptTemplate, placeholders=placeholders, options=options)
|
||||
|
||||
# Log result review response received
|
||||
logger.info("=== RESULT REVIEW AI RESPONSE RECEIVED ===")
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ class ReactMode(BaseMode):
|
|||
maxProcessingTime=30
|
||||
)
|
||||
|
||||
response = await self.services.ai.callAi(
|
||||
response = await self.services.ai.coreAi.callAiPlanning(
|
||||
prompt=promptTemplate,
|
||||
placeholders=placeholders,
|
||||
options=options
|
||||
|
|
@ -313,7 +313,7 @@ class ReactMode(BaseMode):
|
|||
resultFormat="json" # Explicitly request JSON format
|
||||
)
|
||||
|
||||
paramsResp = await self.services.ai.callAi(
|
||||
paramsResp = await self.services.ai.coreAi.callAiPlanning(
|
||||
prompt=promptTemplate,
|
||||
placeholders=placeholders,
|
||||
options=options
|
||||
|
|
@ -625,7 +625,7 @@ class ReactMode(BaseMode):
|
|||
maxProcessingTime=30
|
||||
)
|
||||
|
||||
resp = await self.services.ai.callAi(
|
||||
resp = await self.services.ai.coreAi.callAiPlanning(
|
||||
prompt=promptTemplate,
|
||||
placeholders=placeholders,
|
||||
options=options
|
||||
|
|
@ -719,8 +719,9 @@ User language: {userLanguage}
|
|||
Return only the user-friendly message, no technical details."""
|
||||
|
||||
# Call AI to generate user-friendly message
|
||||
response = await self.services.ai.callAi(
|
||||
response = await self.services.ai.coreAi.callAiPlanning(
|
||||
prompt=prompt,
|
||||
placeholders=None,
|
||||
options=AiCallOptions(
|
||||
operationType=OperationType.GENERATE_CONTENT,
|
||||
priority=Priority.SPEED,
|
||||
|
|
@ -759,8 +760,9 @@ Result context: {resultContext}
|
|||
Return only the user-friendly message, no technical details."""
|
||||
|
||||
# Call AI to generate user-friendly result message
|
||||
response = await self.services.ai.callAi(
|
||||
response = await self.services.ai.coreAi.callAiPlanning(
|
||||
prompt=prompt,
|
||||
placeholders=None,
|
||||
options=AiCallOptions(
|
||||
operationType=OperationType.GENERATE_CONTENT,
|
||||
priority=Priority.SPEED,
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ class WorkflowManager:
|
|||
)
|
||||
|
||||
# Call AI analyzer
|
||||
aiResponse = await self.services.ai.callAi(prompt=analyzerPrompt)
|
||||
aiResponse = await self.services.ai.coreAi.callAiPlanning(prompt=analyzerPrompt, placeholders=None, options=None)
|
||||
|
||||
detectedLanguage = None
|
||||
normalizedRequest = None
|
||||
|
|
|
|||
|
|
@ -1,258 +0,0 @@
|
|||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
# Add the project root to the sys.path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
|
||||
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType
|
||||
from modules.datamodels.datamodelChat import ChatDocument
|
||||
from modules.services.serviceAi.subCoreAi import SubCoreAi
|
||||
|
||||
class MockAiObjects:
|
||||
def __init__(self, responses):
|
||||
self.responses = responses
|
||||
self.call_count = 0
|
||||
|
||||
async def call(self, request: AiCallRequest):
|
||||
if self.call_count < len(self.responses):
|
||||
response_content = self.responses[self.call_count]
|
||||
self.call_count += 1
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = response_content
|
||||
mock_response.modelName = "mock-model"
|
||||
mock_response.priceUsd = 0.001
|
||||
mock_response.processingTime = 0.1
|
||||
print(f" Mock AI Call {self.call_count}: Responding with partial result (length: {len(response_content)})")
|
||||
return mock_response
|
||||
else:
|
||||
print(" Mock AI Call: No more mock responses, returning empty.")
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = ""
|
||||
return mock_response
|
||||
|
||||
class MockServices:
|
||||
def __init__(self):
|
||||
self.currentWorkflow = MagicMock()
|
||||
self.currentWorkflow.id = "test_workflow_123"
|
||||
self.workflow = MagicMock()
|
||||
self.workflow.createProgressLogger.return_value = MagicMock()
|
||||
self.workflow.storeWorkflowStat = AsyncMock()
|
||||
self.ai = MagicMock()
|
||||
self.ai.sanitizePromptContent.side_effect = lambda content, type: content
|
||||
self.utils = MagicMock()
|
||||
self.utils.debugLogToFile.side_effect = lambda msg, tag: print(f" DEBUG ({tag}): {msg}")
|
||||
self.utils.configGet.return_value = False # Disable debug files for tests
|
||||
|
||||
class MockDocumentProcessor:
|
||||
async def callAiText(self, prompt, documents, options):
|
||||
return "Extracted content from documents: Sample text content"
|
||||
|
||||
async def test_unified_architecture():
|
||||
print("\n=== Testing Unified Architecture ===")
|
||||
|
||||
# Mock responses: 1 for generation prompt building + 2 for actual generation
|
||||
mock_responses = [
|
||||
# Response 1: Generation prompt building
|
||||
"Generate JSON content that creates a structured document with prime numbers in a table format. Use the canonical JSON format with sections and elements.",
|
||||
|
||||
# Response 2: First part of generation
|
||||
"""{
|
||||
"metadata": {
|
||||
"title": "Prime Numbers List",
|
||||
"splitStrategy": "single_document",
|
||||
"source_documents": [],
|
||||
"extraction_method": "ai_generation"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"id": "doc_primes_1_500",
|
||||
"title": "Prime Numbers 1-500",
|
||||
"filename": "primes_1_500.docx",
|
||||
"sections": [
|
||||
{
|
||||
"id": "section_1",
|
||||
"content_type": "table",
|
||||
"elements": [
|
||||
{
|
||||
"headers": ["Number", "Prime"],
|
||||
"rows": [
|
||||
["1", "2"], ["2", "3"], ["3", "5"], ["4", "7"], ["5", "11"]
|
||||
]
|
||||
}
|
||||
],
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
} [CONTINUE: Generate remaining prime numbers from 501 to 1000]""",
|
||||
|
||||
# Response 3: Second part of generation
|
||||
"""{
|
||||
"metadata": {
|
||||
"title": "Prime Numbers List",
|
||||
"splitStrategy": "single_document",
|
||||
"source_documents": [],
|
||||
"extraction_method": "ai_generation"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"id": "doc_primes_501_1000",
|
||||
"title": "Prime Numbers 501-1000",
|
||||
"filename": "primes_501_1000.docx",
|
||||
"sections": [
|
||||
{
|
||||
"id": "section_2",
|
||||
"content_type": "table",
|
||||
"elements": [
|
||||
{
|
||||
"headers": ["Number", "Prime"],
|
||||
"rows": [
|
||||
["501", "3571"], ["502", "3572"], ["503", "3581"]
|
||||
]
|
||||
}
|
||||
],
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"""
|
||||
]
|
||||
|
||||
mock_ai_objects = MockAiObjects(mock_responses)
|
||||
mock_services = MockServices()
|
||||
mock_document_processor = MockDocumentProcessor()
|
||||
|
||||
core_ai_service = SubCoreAi(mock_services, mock_ai_objects)
|
||||
|
||||
prompt = "Generate the first 1000 prime numbers and arrange them in a structured table format."
|
||||
options = AiCallOptions(operationType=OperationType.GENERATE_CONTENT)
|
||||
output_format = "docx"
|
||||
title = "Prime Numbers List"
|
||||
|
||||
print(f"User Prompt: '{prompt}'")
|
||||
print("Testing unified architecture with direct generation (no documents)...")
|
||||
|
||||
# Test the unified generation method directly
|
||||
result = await core_ai_service._callAiUnifiedGeneration(prompt, None, options, output_format, title)
|
||||
|
||||
print("\n--- Generated JSON Result ---")
|
||||
print(f"Result length: {len(result)} characters")
|
||||
print(f"Result preview: {result[:300]}...")
|
||||
|
||||
# Verify it's valid JSON
|
||||
import json
|
||||
try:
|
||||
parsed_result = json.loads(result)
|
||||
print(f"✅ Valid JSON with {len(parsed_result.get('documents', []))} documents")
|
||||
|
||||
# Verify it's using the multi-document format
|
||||
if "documents" in parsed_result and "metadata" in parsed_result:
|
||||
print("✅ Using unified multi-document format")
|
||||
print("✅ Architecture is properly unified!")
|
||||
return True
|
||||
else:
|
||||
print("❌ Not using multi-document format")
|
||||
return False
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Invalid JSON: {str(e)}")
|
||||
return False
|
||||
|
||||
async def test_with_documents():
|
||||
print("\n=== Testing Unified Architecture WITH Documents ===")
|
||||
|
||||
# Mock responses: 1 for generation prompt building + 1 for actual generation
|
||||
mock_responses = [
|
||||
# Response 1: Generation prompt building
|
||||
"Generate JSON content that creates a comprehensive fruit analysis report based on the extracted content. Use the canonical JSON format with sections and elements.",
|
||||
|
||||
# Response 2: Generation with extracted content
|
||||
"""{
|
||||
"metadata": {
|
||||
"title": "Fruit Analysis Report",
|
||||
"splitStrategy": "single_document",
|
||||
"source_documents": ["doc1"],
|
||||
"extraction_method": "ai_generation"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"id": "doc_fruit_analysis",
|
||||
"title": "Fruit Analysis Report",
|
||||
"filename": "fruit_analysis.docx",
|
||||
"sections": [
|
||||
{
|
||||
"id": "section_1",
|
||||
"content_type": "paragraph",
|
||||
"elements": [
|
||||
{
|
||||
"text": "Based on the extracted content, here is a comprehensive fruit analysis..."
|
||||
}
|
||||
],
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"""
|
||||
]
|
||||
|
||||
mock_ai_objects = MockAiObjects(mock_responses)
|
||||
mock_services = MockServices()
|
||||
mock_document_processor = MockDocumentProcessor()
|
||||
|
||||
core_ai_service = SubCoreAi(mock_services, mock_ai_objects)
|
||||
|
||||
prompt = "Extract all fruit information and create a comprehensive analysis report."
|
||||
options = AiCallOptions(operationType=OperationType.GENERATE_CONTENT)
|
||||
output_format = "docx"
|
||||
title = "Fruit Analysis Report"
|
||||
|
||||
print(f"User Prompt: '{prompt}'")
|
||||
print("Testing unified architecture with document extraction...")
|
||||
|
||||
# Test the unified generation method with extracted content
|
||||
result = await core_ai_service._callAiUnifiedGeneration(prompt, "Sample fruit data: apples, oranges, bananas", options, output_format, title)
|
||||
|
||||
print("\n--- Generated JSON Result ---")
|
||||
print(f"Result length: {len(result)} characters")
|
||||
print(f"Result preview: {result[:300]}...")
|
||||
|
||||
# Verify it's valid JSON
|
||||
import json
|
||||
try:
|
||||
parsed_result = json.loads(result)
|
||||
print(f"✅ Valid JSON with {len(parsed_result.get('documents', []))} documents")
|
||||
|
||||
# Verify it's using the multi-document format
|
||||
if "documents" in parsed_result and "metadata" in parsed_result:
|
||||
print("✅ Using unified multi-document format")
|
||||
print("✅ Architecture is properly unified!")
|
||||
return True
|
||||
else:
|
||||
print("❌ Not using multi-document format")
|
||||
return False
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Invalid JSON: {str(e)}")
|
||||
return False
|
||||
|
||||
async def main():
|
||||
print("🚀 Testing Unified Architecture Implementation")
|
||||
print("=" * 60)
|
||||
|
||||
success1 = await test_unified_architecture()
|
||||
success2 = await test_with_documents()
|
||||
|
||||
if success1 and success2:
|
||||
print("\n🎉 ALL TESTS PASSED! Unified architecture is properly implemented.")
|
||||
print("✅ Single document = multi-document with n=1")
|
||||
print("✅ Always uses multi-document JSON format")
|
||||
print("✅ Continuation logic works for long responses")
|
||||
print("✅ Both scenarios (with/without documents) work")
|
||||
else:
|
||||
print("\n❌ Some tests failed. Please check the implementation.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Loading…
Reference in a new issue