# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Code Generation Path Handles code generation with multi-file project support, dependency handling, and proper cross-file references. """ import json import logging import time import re from typing import Dict, Any, List, Optional from modules.datamodels.datamodelWorkflow import AiResponse, AiResponseMetadata, DocumentData from modules.datamodels.datamodelExtraction import ContentPart from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum from modules.shared.jsonUtils import extractJsonString logger = logging.getLogger(__name__) class CodeGenerationPath: """Code generation path.""" def __init__(self, services): self.services = services async def generateCode( self, userPrompt: str, outputFormat: str = None, contentParts: Optional[List[ContentPart]] = None, title: str = "Generated Code", parentOperationId: Optional[str] = None ) -> AiResponse: """ Generate code files with multi-file project support. Returns: AiResponse with code files as documents """ # Create operation ID workflowId = self.services.workflow.id if self.services.workflow else f"no-workflow-{int(time.time())}" codeOperationId = f"code_gen_{workflowId}_{int(time.time())}" # Start progress tracking self.services.chat.progressLogStart( codeOperationId, "Code Generation", "Code Generation", f"Format: {outputFormat or 'txt'}", parentOperationId=parentOperationId ) try: # Detect language and project type from prompt or outputFormat language, projectType = self._detectLanguageAndProjectType(userPrompt, outputFormat) # Phase 1: Code structure generation (with looping) self.services.chat.progressLogUpdate(codeOperationId, 0.2, "Generating code structure") codeStructure = await self._generateCodeStructure( userPrompt=userPrompt, language=language, outputFormat=outputFormat, contentParts=contentParts ) # Phase 2: Code content generation (with dependency handling) self.services.chat.progressLogUpdate(codeOperationId, 0.5, "Generating code content") codeFiles = await self._generateCodeContent( codeStructure, codeOperationId, userPrompt=userPrompt, contentParts=contentParts ) # Phase 3: Code formatting & validation self.services.chat.progressLogUpdate(codeOperationId, 0.8, "Formatting code files") formattedFiles = await self._formatAndValidateCode(codeFiles) # Phase 4: Code Rendering (Renderer-Based) self.services.chat.progressLogUpdate(codeOperationId, 0.9, "Rendering code files") # Group files by format filesByFormat = {} for file in formattedFiles: fileType = file.get("fileType", outputFormat or "txt") if fileType not in filesByFormat: filesByFormat[fileType] = [] filesByFormat[fileType].append(file) # Render each format group using appropriate renderer allRenderedDocuments = [] for fileType, files in filesByFormat.items(): # Get renderer for this format renderer = self._getCodeRenderer(fileType) if renderer: # Use code renderer renderedDocs = await renderer.renderCodeFiles( codeFiles=files, metadata=codeStructure.get("metadata", {}), userPrompt=userPrompt ) allRenderedDocuments.extend(renderedDocs) else: # Fallback: output directly (for formats without renderers) for file in files: mimeType = self._getMimeType(file.get("fileType", "txt")) content = file.get("content", "") contentBytes = content.encode('utf-8') if isinstance(content, str) else content from modules.datamodels.datamodelDocument import RenderedDocument allRenderedDocuments.append( RenderedDocument( documentData=contentBytes, mimeType=mimeType, filename=file.get("filename", "generated.txt"), metadata=codeStructure.get("metadata", {}) ) ) # Convert RenderedDocument to DocumentData documents = [] for renderedDoc in allRenderedDocuments: documents.append(DocumentData( documentName=renderedDoc.filename, documentData=renderedDoc.documentData, mimeType=renderedDoc.mimeType, sourceJson=renderedDoc.metadata if hasattr(renderedDoc, 'metadata') else None )) metadata = AiResponseMetadata( title=title, operationType=OperationTypeEnum.DATA_GENERATE.value ) # Create summary JSON for content field summaryContent = { "type": "code_generation", "metadata": codeStructure.get("metadata", {}), "files": [ { "filename": doc.documentName, "mimeType": doc.mimeType } for doc in documents ], "fileCount": len(documents) } self.services.chat.progressLogFinish(codeOperationId, True) return AiResponse( documents=documents, content=json.dumps(summaryContent, ensure_ascii=False), metadata=metadata ) except Exception as e: logger.error(f"Error in code generation: {str(e)}") self.services.chat.progressLogFinish(codeOperationId, False) raise def _detectLanguageAndProjectType(self, userPrompt: str, outputFormat: Optional[str]) -> tuple: """Detect programming language and project type from prompt or format.""" promptLower = userPrompt.lower() # Detect language language = None if outputFormat: if outputFormat == "py": language = "python" elif outputFormat in ["js", "ts"]: language = outputFormat elif outputFormat == "html": language = "html" if not language: if "python" in promptLower or ".py" in promptLower: language = "python" elif "javascript" in promptLower or ".js" in promptLower: language = "javascript" elif "typescript" in promptLower or ".ts" in promptLower: language = "typescript" elif "html" in promptLower: language = "html" else: language = "python" # Default # Detect project type projectType = "single_file" if "multi" in promptLower or "multiple files" in promptLower or "project" in promptLower: projectType = "multi_file" return language, projectType async def _generateCodeStructure( self, userPrompt: str, language: str, outputFormat: Optional[str], contentParts: Optional[List[ContentPart]] ) -> Dict[str, Any]: """Generate code structure using looping system.""" # Build content parts index (similar to document generation) contentPartsIndex = "" if contentParts: validParts = [] for part in contentParts: contentFormat = part.metadata.get("contentFormat", "unknown") originalFileName = part.metadata.get('originalFileName', 'N/A') # Include reference parts and parts with data if contentFormat == "reference" or (part.data and len(str(part.data).strip()) > 0): validParts.append(part) if validParts: contentPartsIndex = "\n## AVAILABLE CONTENT PARTS\n" for i, part in enumerate(validParts, 1): contentFormat = part.metadata.get("contentFormat", "unknown") originalFileName = part.metadata.get('originalFileName', 'N/A') contentPartsIndex += f"\n{i}. ContentPart ID: {part.id}\n" contentPartsIndex += f" Format: {contentFormat}\n" contentPartsIndex += f" Type: {part.typeGroup}\n" contentPartsIndex += f" MIME Type: {part.mimeType or 'N/A'}\n" contentPartsIndex += f" Source: {part.metadata.get('documentId', 'unknown')}\n" contentPartsIndex += f" Original file name: {originalFileName}\n" contentPartsIndex += f" Usage hint: {part.metadata.get('usageHint', 'N/A')}\n" if not contentPartsIndex: contentPartsIndex = "\n(No content parts available)" # Create template structure explicitly (not extracted from prompt) templateStructure = f"""{{ "metadata": {{ "language": "{language}", "projectType": "single_file|multi_file", "projectName": "" }}, "files": [ {{ "id": "", "filename": "", "fileType": "", "dependencies": [], "imports": [], "functions": [], "classes": [] }} ] }}""" # Build structure generation prompt structurePrompt = f"""# TASK: Generate Code Project Structure This is a PLANNING task. Return EXACTLY ONE complete JSON object. Do not generate multiple JSON objects, alternatives, or variations. Do not use separators like "---" between JSON objects. ## USER REQUEST (for context) ``` {userPrompt} ``` {contentPartsIndex} ## LANGUAGE {language} ## TASK DESCRIPTION Analyze the USER REQUEST above and create a project structure that fulfills ALL requirements mentioned in the request. IMPORTANT: If the request mentions multiple files (e.g., "3 files", "config.json and customers.json", etc.), you MUST include ALL requested files in the files array. Set projectType to "multi_file" when multiple files are requested. ## CONTENT PARTS USAGE (if available) If AVAILABLE CONTENT PARTS are listed above, use them to inform the file structure: **Analyzing Content Parts:** - Review each ContentPart's format, type, original file name, and usage hint - Content parts with "reference" format = documents/images that will be processed/extracted - Content parts with "extracted" format = pre-processed data ready to use - Content parts with "object" format = images/documents to be displayed or processed **Mapping Content Parts to Files:** - If content parts contain data (e.g., expense receipts, customer lists), create data files (JSON/CSV) that will store/represent that data - If content parts are documents to be processed (e.g., PDFs), you may need code files that parse/process them - Use the original file names and usage hints to determine appropriate filenames and file types **Populating File Structure Fields:** - **dependencies**: List file IDs that this file depends on (e.g., if a Python script reads a JSON config file, the script depends on the config file) - **imports**: For code files, list imports needed based on content parts (e.g., if processing PDFs: ["import PyPDF2"], if processing CSV: ["import csv"], if processing JSON: ["import json"]) - **functions**: For CODE files only - list function signatures if the USER REQUEST specifies functionality (e.g., {{"name": "parseReceipt", "signature": "def parseReceipt(pdf_path: str) -> dict"}}) - **classes**: For CODE files only - list class definitions if the USER REQUEST specifies OOP structure - **functions/classes for DATA files**: Leave as empty arrays [] - data files (JSON/CSV/XML) don't contain executable code ## FILE STRUCTURE REQUIREMENTS Create a JSON structure with: 1. metadata: {{"language": "{language}", "projectType": "single_file|multi_file", "projectName": "..."}} - projectName: Derive from USER REQUEST or content parts (e.g., "expense-tracker", "customer-manager") 2. files: Array of file structures, each with: - id: Unique identifier (e.g., "file_1", "file_2") - filename: File name matching USER REQUEST requirements (e.g., "config.json", "customers.json", "expenses.csv") - fileType: File extension matching the requested format (e.g., "json", "py", "js", "csv", "xml") - dependencies: List of file IDs this file depends on (for multi-file projects where files reference each other) - imports: List of import statements that this file will need (e.g., ["import json", "import csv"] for Python files processing JSON/CSV) - functions: Array of function signatures {{"name": "...", "signature": "..."}} - ONLY if the file will contain executable code (not for pure data files like JSON/CSV) - classes: Array of class definitions {{"name": "...", "signature": "..."}} - ONLY if the file will contain executable code (not for pure data files like JSON/CSV) IMPORTANT FOR DATA FILES (JSON, CSV, XML): - For pure data files (config.json, customers.json, expenses.csv), leave functions and classes as empty arrays [] - These files contain structured data, not executable code - Use imports only if the file will be processed by code (e.g., a Python script that reads the CSV) IMPORTANT FOR CODE FILES (Python, JavaScript, etc.): - Include functions/classes if the USER REQUEST specifies functionality - Use dependencies to indicate which data files this code file reads/processes - Use imports to specify what libraries/modules are needed For single-file projects, return one file. For multi-file projects, include ALL requested files in the files array. Return ONLY valid JSON matching the request above. """ # Build continuation prompt builder async def buildCodeStructurePromptWithContinuation( continuationContext: Any, templateStructure: str, basePrompt: str ) -> str: """Build code structure prompt with continuation context. Uses unified signature. Note: All initial context (userPrompt, contentParts, etc.) is already contained in basePrompt. This function only adds continuation-specific instructions. """ # Extract continuation context fields (only what's needed for continuation) incompletePart = continuationContext.incomplete_part lastRawJson = continuationContext.last_raw_json # Generate both overlap context and hierarchy context using jsonContinuation overlapContext = "" unifiedContext = "" if lastRawJson: # Get contexts directly from jsonContinuation from modules.shared.jsonContinuation import getContexts contexts = getContexts(lastRawJson) overlapContext = contexts.overlapContext unifiedContext = contexts.hierarchyContextForPrompt elif incompletePart: unifiedContext = incompletePart else: unifiedContext = "Unable to extract context - response was completely broken" # Build unified continuation prompt format continuationPrompt = f"""{basePrompt} --- CONTINUATION REQUEST --- The previous JSON response was incomplete. Continue from where it stopped. Context showing structure hierarchy with cut point: ``` {unifiedContext} ``` Overlap Requirement: To ensure proper merging, your response MUST start EXACTLY with the overlap context shown below, then continue with new content. Overlap context (start your response with this exact text): ```json {overlapContext if overlapContext else "No overlap context available"} ``` TASK: 1. Start your response EXACTLY with the overlap context shown above (character by character) 2. Continue seamlessly from where the overlap context ends 3. Complete the remaining content following the JSON structure template above 4. Return ONLY valid JSON following the structure template - no overlap/continuation wrapper objects CRITICAL: - Your response MUST begin with the exact overlap context text (this enables automatic merging) - Continue seamlessly after the overlap context with new content - Your response must be valid JSON matching the structure template above""" return continuationPrompt # Use generic looping system with code_structure use case options = AiCallOptions( operationType=OperationTypeEnum.DATA_GENERATE, resultFormat="json" ) structureJson = await self.services.ai.callAiWithLooping( prompt=structurePrompt, options=options, promptBuilder=buildCodeStructurePromptWithContinuation, promptArgs={ "userPrompt": userPrompt, "contentParts": contentParts, "templateStructure": templateStructure, "basePrompt": structurePrompt }, useCaseId="code_structure", debugPrefix="code_structure_generation", contentParts=contentParts ) # Extract JSON from markdown fences if present extractedJson = extractJsonString(structureJson) parsed = json.loads(extractedJson) return parsed async def _generateCodeContent( self, codeStructure: Dict[str, Any], parentOperationId: str, userPrompt: str = None, contentParts: Optional[List[ContentPart]] = None ) -> List[Dict[str, Any]]: """Generate code content for each file with dependency handling.""" files = codeStructure.get("files", []) metadata = codeStructure.get("metadata", {}) if not files: raise ValueError("No files found in code structure") # Step 1: Resolve dependency order orderedFiles = self._resolveDependencyOrder(files) # Step 2: Generate dependency files first (requirements.txt, package.json, etc.) dependencyFiles = await self._generateDependencyFiles(metadata, orderedFiles) # Step 3: Generate code files in dependency order (not fully parallel) codeFiles = [] generatedFileContext = {} # Track what's been generated for cross-file references for idx, fileStructure in enumerate(orderedFiles): # Update progress progress = 0.5 + (0.4 * (idx / len(orderedFiles))) self.services.chat.progressLogUpdate( parentOperationId, progress, f"Generating {fileStructure.get('filename', 'file')}" ) # Provide context about already-generated files for proper imports fileContext = self._buildFileContext(generatedFileContext, fileStructure) # Generate this file with context fileContent = await self._generateSingleFileContent( fileStructure, fileContext=fileContext, allFilesStructure=orderedFiles, metadata=metadata, userPrompt=userPrompt, contentParts=contentParts ) codeFiles.append(fileContent) # Update context with generated file info (for next files) generatedFileContext[fileStructure["id"]] = { "filename": fileContent.get("filename", fileStructure.get("filename")), "functions": fileContent.get("functions", []), "classes": fileContent.get("classes", []), "exports": fileContent.get("exports", []) } # Combine dependency files and code files return dependencyFiles + codeFiles def _resolveDependencyOrder(self, files: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """Resolve file generation order based on dependencies using topological sort.""" # Build dependency graph fileMap = {f["id"]: f for f in files} dependencies = {} for file in files: fileId = file["id"] deps = file.get("dependencies", []) # List of file IDs this file depends on dependencies[fileId] = deps # Topological sort ordered = [] visited = set() tempMark = set() def visit(fileId: str): if fileId in tempMark: # Circular dependency detected - break it logger.warning(f"Circular dependency detected involving {fileId}") return if fileId in visited: return tempMark.add(fileId) for depId in dependencies.get(fileId, []): if depId in fileMap: visit(depId) tempMark.remove(fileId) visited.add(fileId) ordered.append(fileMap[fileId]) for file in files: if file["id"] not in visited: visit(file["id"]) return ordered async def _generateDependencyFiles( self, metadata: Dict[str, Any], files: List[Dict[str, Any]] ) -> List[Dict[str, Any]]: """Generate dependency files (requirements.txt, package.json, etc.).""" language = metadata.get("language", "").lower() dependencyFiles = [] # Generate requirements.txt for Python if language in ["python", "py"]: requirementsContent = await self._generateRequirementsTxt(files) if requirementsContent: dependencyFiles.append({ "filename": "requirements.txt", "content": requirementsContent, "fileType": "txt", "id": "requirements_txt" }) # Generate package.json for JavaScript/TypeScript elif language in ["javascript", "typescript", "js", "ts"]: packageJson = await self._generatePackageJson(files, metadata) if packageJson: dependencyFiles.append({ "filename": "package.json", "content": json.dumps(packageJson, indent=2), "fileType": "json", "id": "package_json" }) return dependencyFiles async def _generateRequirementsTxt( self, files: List[Dict[str, Any]] ) -> Optional[str]: """Generate requirements.txt content from Python imports.""" pythonPackages = set() for file in files: imports = file.get("imports", []) if isinstance(imports, list): for imp in imports: if isinstance(imp, str): # Extract package name from import # Handle: "from flask import", "import flask", "from flask import Flask" imp = imp.strip() if "import" in imp: if "from" in imp: # "from package import ..." parts = imp.split("from") if len(parts) > 1: package = parts[1].split("import")[0].strip() if package and not package.startswith("."): pythonPackages.add(package.split(".")[0]) # Get root package else: # "import package" or "import package.module" parts = imp.split("import") if len(parts) > 1: package = parts[1].strip().split(".")[0].strip() if package and not package.startswith("."): pythonPackages.add(package) if pythonPackages: return "\n".join(sorted(pythonPackages)) return None async def _generatePackageJson( self, files: List[Dict[str, Any]], metadata: Dict[str, Any] ) -> Optional[Dict[str, Any]]: """Generate package.json content from JavaScript/TypeScript imports.""" npmPackages = {} for file in files: imports = file.get("imports", []) if isinstance(imports, list): for imp in imports: if isinstance(imp, str): # Extract npm package from import # Handle: "import express from 'express'", "const express = require('express')" imp = imp.strip() if "from" in imp: # ES6 import: "import ... from 'package'" parts = imp.split("from") if len(parts) > 1: package = parts[1].strip().strip("'\"") if package and not package.startswith(".") and not package.startswith("/"): npmPackages[package] = "*" elif "require" in imp: # CommonJS: "require('package')" match = re.search(r"require\(['\"]([^'\"]+)['\"]\)", imp) if match: package = match.group(1) if not package.startswith(".") and not package.startswith("/"): npmPackages[package] = "*" if npmPackages: return { "name": metadata.get("projectName", "generated-project"), "version": "1.0.0", "dependencies": npmPackages } return None def _buildFileContext( self, generatedFileContext: Dict[str, Dict[str, Any]], currentFile: Dict[str, Any] ) -> Dict[str, Any]: """Build context about other files for proper imports/references.""" context = { "availableFiles": [], "availableFunctions": {}, "availableClasses": {} } # Add info about already-generated files for fileId, fileInfo in generatedFileContext.items(): context["availableFiles"].append({ "id": fileId, "filename": fileInfo["filename"], "functions": fileInfo.get("functions", []), "classes": fileInfo.get("classes", []), "exports": fileInfo.get("exports", []) }) # Build function/class maps for easy lookup for func in fileInfo.get("functions", []): funcName = func.get("name", "") if funcName: context["availableFunctions"][funcName] = { "file": fileInfo["filename"], "signature": func.get("signature", "") } for cls in fileInfo.get("classes", []): className = cls.get("name", "") if className: context["availableClasses"][className] = { "file": fileInfo["filename"] } return context async def _generateSingleFileContent( self, fileStructure: Dict[str, Any], fileContext: Dict[str, Any] = None, allFilesStructure: List[Dict[str, Any]] = None, metadata: Dict[str, Any] = None, userPrompt: str = None, contentParts: Optional[List[ContentPart]] = None ) -> Dict[str, Any]: """Generate code content for a single file with context about other files.""" # Build prompt with context about other files for proper imports filename = fileStructure.get("filename", "generated.py") fileType = fileStructure.get("fileType", "py") dependencies = fileStructure.get("dependencies", []) functions = fileStructure.get("functions", []) classes = fileStructure.get("classes", []) contextInfo = "" if fileContext and fileContext.get("availableFiles"): contextInfo = "\n\nAvailable files and their exports:\n" for fileInfo in fileContext["availableFiles"]: contextInfo += f"- {fileInfo['filename']}: " funcs = [f.get("name", "") for f in fileInfo.get("functions", [])] cls = [c.get("name", "") for c in fileInfo.get("classes", [])] exports = [] if funcs: exports.extend(funcs) if cls: exports.extend(cls) if exports: contextInfo += ", ".join(exports) contextInfo += "\n" # Build content parts section if available contentPartsSection = "" if contentParts: relevantParts = [] for part in contentParts: # Include parts that might be relevant to this file usageHint = part.metadata.get('usageHint', '').lower() originalFileName = part.metadata.get('originalFileName', '').lower() filenameLower = filename.lower() # Check if this content part is relevant to this file if (filenameLower in usageHint or filenameLower in originalFileName or part.metadata.get('contentFormat') == 'reference' or (part.data and len(str(part.data).strip()) > 0)): relevantParts.append(part) if relevantParts: contentPartsSection = "\n## AVAILABLE CONTENT PARTS\n" for i, part in enumerate(relevantParts, 1): contentFormat = part.metadata.get("contentFormat", "unknown") originalFileName = part.metadata.get('originalFileName', 'N/A') contentPartsSection += f"\n{i}. ContentPart ID: {part.id}\n" contentPartsSection += f" Format: {contentFormat}\n" contentPartsSection += f" Type: {part.typeGroup}\n" contentPartsSection += f" Original file name: {originalFileName}\n" contentPartsSection += f" Usage hint: {part.metadata.get('usageHint', 'N/A')}\n" # Include actual content if it's small enough (for data files like CSV, JSON) if part.data and isinstance(part.data, str) and len(part.data) < 2000: contentPartsSection += f" Content preview: {part.data[:500]}...\n" # Build user request section userRequestSection = "" if userPrompt: userRequestSection = f""" ## ORIGINAL USER REQUEST ``` {userPrompt} ``` """ # Create template structure explicitly (not extracted from prompt) templateStructure = f"""{{ "files": [ {{ "filename": "{filename}", "content": "// Complete code here", "functions": {json.dumps(functions, indent=2) if functions else '[]'}, "classes": {json.dumps(classes, indent=2) if classes else '[]'} }} ] }}""" # Build base prompt contentPrompt = f"""# TASK: Generate Code File Content Generate complete, executable code for the file: {filename} {userRequestSection}## FILE SPECIFICATIONS File Type: {fileType} Language: {metadata.get('language', 'python') if metadata else 'python'} {contentPartsSection} Required functions: {json.dumps(functions, indent=2) if functions else 'None specified'} Required classes: {json.dumps(classes, indent=2) if classes else 'None specified'} Dependencies on other files: {', '.join(dependencies) if dependencies else 'None'} {contextInfo} Generate complete, production-ready code with: 1. Proper imports (including imports from other files in the project if dependencies exist) 2. All required functions and classes 3. Error handling 4. Documentation/docstrings 5. Type hints where appropriate Return ONLY valid JSON in this format: {templateStructure} """ # Build continuation prompt builder async def buildCodeContentPromptWithContinuation( continuationContext: Any, templateStructure: str, basePrompt: str ) -> str: """Build code content prompt with continuation context. Uses unified signature. Note: All initial context (filename, fileType, functions, etc.) is already contained in basePrompt. This function only adds continuation-specific instructions. """ # Extract continuation context fields (only what's needed for continuation) incompletePart = continuationContext.incomplete_part lastRawJson = continuationContext.last_raw_json # Generate both overlap context and hierarchy context using jsonContinuation overlapContext = "" unifiedContext = "" if lastRawJson: # Get contexts directly from jsonContinuation from modules.shared.jsonContinuation import getContexts contexts = getContexts(lastRawJson) overlapContext = contexts.overlapContext unifiedContext = contexts.hierarchyContextForPrompt elif incompletePart: unifiedContext = incompletePart else: unifiedContext = "Unable to extract context - response was completely broken" # Build unified continuation prompt format continuationPrompt = f"""{basePrompt} --- CONTINUATION REQUEST --- The previous JSON response was incomplete. Continue from where it stopped. Context showing structure hierarchy with cut point: ``` {unifiedContext} ``` Overlap Requirement: To ensure proper merging, your response MUST start EXACTLY with the overlap context shown below, then continue with new content. Overlap context (start your response with this exact text): ```json {overlapContext if overlapContext else "No overlap context available"} ``` TASK: 1. Start your response EXACTLY with the overlap context shown above (character by character) 2. Continue seamlessly from where the overlap context ends 3. Complete the remaining content following the JSON structure template above 4. Return ONLY valid JSON following the structure template - no overlap/continuation wrapper objects CRITICAL: - Your response MUST begin with the exact overlap context text (this enables automatic merging) - Continue seamlessly after the overlap context with new content - Your response must be valid JSON matching the structure template above""" return continuationPrompt # Use generic looping system with code_content use case options = AiCallOptions( operationType=OperationTypeEnum.DATA_GENERATE, resultFormat="json" ) contentJson = await self.services.ai.callAiWithLooping( prompt=contentPrompt, options=options, promptBuilder=buildCodeContentPromptWithContinuation, promptArgs={ "filename": filename, "fileType": fileType, "functions": functions, "classes": classes, "dependencies": dependencies, "metadata": metadata, "userPrompt": userPrompt, "contentParts": contentParts, "contextInfo": contextInfo, "templateStructure": templateStructure, "basePrompt": contentPrompt }, useCaseId="code_content", debugPrefix=f"code_content_{fileStructure.get('id', 'file')}", ) # Extract JSON from markdown fences if present extractedJson = extractJsonString(contentJson) parsed = json.loads(extractedJson) # Extract file content and metadata files = parsed.get("files", []) if files and len(files) > 0: fileData = files[0] return { "filename": fileData.get("filename", filename), "content": fileData.get("content", ""), "fileType": fileType, "functions": fileData.get("functions", functions), "classes": fileData.get("classes", classes), "id": fileStructure.get("id") } # Fallback if structure is different return { "filename": filename, "content": parsed.get("content", ""), "fileType": fileType, "functions": functions, "classes": classes, "id": fileStructure.get("id") } async def _formatAndValidateCode(self, codeFiles: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """Format and validate generated code files.""" # For now, just return files as-is # TODO: Add code formatting (black, prettier, etc.) and validation formatted = [] for file in codeFiles: content = file.get("content", "") # Basic cleanup: remove markdown code fences if present if isinstance(content, str): content = re.sub(r'^```[\w]*\n', '', content, flags=re.MULTILINE) content = re.sub(r'\n```$', '', content, flags=re.MULTILINE) file["content"] = content.strip() formatted.append(file) return formatted def _getMimeType(self, fileType: str) -> str: """Get MIME type for file type.""" mimeTypes = { "py": "text/x-python", "js": "application/javascript", "ts": "application/typescript", "html": "text/html", "css": "text/css", "json": "application/json", "txt": "text/plain", "md": "text/markdown", "java": "text/x-java-source", "cpp": "text/x-c++src", "c": "text/x-csrc", "csv": "text/csv", "xml": "application/xml" } return mimeTypes.get(fileType.lower(), "text/plain") def _getCodeRenderer(self, fileType: str): """Get code renderer for file type.""" from modules.aichat.serviceGeneration.renderers.registry import getRenderer # Map file types to renderer formats formatMap = { 'json': 'json', 'csv': 'csv', 'xml': 'xml' } rendererFormat = formatMap.get(fileType.lower()) if rendererFormat: renderer = getRenderer(rendererFormat, self.services) # Check if renderer supports code rendering if renderer and hasattr(renderer, 'renderCodeFiles'): return renderer return None