#!/usr/bin/env python3 # Removed asyncio and subprocess - not needed for direct agent calls import json import os import sys import re sys.path.append(os.path.join(os.path.dirname(__file__), 'agents')) from flask import Flask, render_template, request, jsonify from werkzeug.utils import secure_filename from docx import Document import PyPDF2 import io from flask_socketio import SocketIO from mapping_agent import MappingAgent from testcase_agent import TestCaseAgent from sequence_agent import SequenceAgent from conversation_agent import ConversationAgent from content_filter import ContentFilter from jira_client import JIRAClient from document_generator import DocumentGenerator from mermaid_png_generator import MermaidPNGGenerator from chat_logger import ChatLogger from csv_generator import CSVGenerator from stats_manager import StatsManager from email_sender import EmailSender import urllib.parse import base64 def extract_json_from_response(response): """Extract JSON data from agent response""" try: # Look for JSON code block json_pattern = r'```json\s*({.*?})\s*```' match = re.search(json_pattern, response, re.DOTALL) if match: json_str = match.group(1) return json.loads(json_str) except: pass return None app = Flask(__name__) app.config['JSON_AS_ASCII'] = False socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet') # Store chat sessions chat_sessions = {} # Direct agent architecture - no MCP protocol used def load_ui_config(): config_path = os.path.join('config', 'ui_config.json') with open(config_path, 'r') as f: return json.load(f) ui_config = load_ui_config() content_filter = ContentFilter() jira_client = JIRAClient() doc_generator = DocumentGenerator() png_generator = MermaidPNGGenerator() chat_logger = ChatLogger() csv_generator = CSVGenerator() stats_manager = StatsManager() email_sender = EmailSender() # Store last generated output for JIRA posting with user isolation last_generated_output = {} # User session mapping to track which sessions belong to which user user_sessions = {} @app.route('/') def index(): return render_template('chat.html', welcome_message=ui_config['welcome_message']) @app.route('/modern') def modern(): return render_template('chat_modern.html', welcome_message=ui_config['welcome_message']) @app.route('/upload', methods=['POST']) def upload_file(): try: if 'file' not in request.files: return jsonify({'error': 'No file uploaded'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'No file selected'}), 400 # Check file extension and determine agent type filename_lower = file.filename.lower() if filename_lower.endswith(('.json', '.xml')): # Mapping documents agent_type = 'mapping' elif filename_lower.endswith(('.docx', '.pdf', '.txt')): # Determine if it's FRD or process flow based on content or filename if 'frd' in filename_lower or 'requirement' in filename_lower or 'functional' in filename_lower: agent_type = 'testcase' elif 'process' in filename_lower or 'flow' in filename_lower or 'sequence' in filename_lower: agent_type = 'sequence' else: # Default to testcase for document files agent_type = 'testcase' else: return jsonify({'error': 'Unsupported file type. Use .json/.xml for mapping, .docx/.doc/.pdf/.txt for test cases/sequence diagrams'}), 400 # Get additional instructions if provided additional_instructions = request.form.get('instructions', '') # Read file content based on file type filename = secure_filename(file.filename) filename_lower = filename.lower() try: if filename_lower.endswith('.docx'): # Parse DOCX file doc = Document(io.BytesIO(file.read())) file_content = '\n'.join([paragraph.text for paragraph in doc.paragraphs]) elif filename_lower.endswith('.pdf'): # Parse PDF file pdf_reader = PyPDF2.PdfReader(io.BytesIO(file.read())) file_content = '\n'.join([page.extract_text() for page in pdf_reader.pages]) elif filename_lower.endswith('.doc'): # For .doc files, return error message return jsonify({'error': 'Legacy .doc files are not supported. Please convert to .docx format.'}), 400 else: # Plain text files file.seek(0) file_content = file.read().decode('utf-8') except Exception as e: return jsonify({'error': f'Error reading file: {str(e)}'}), 400 # Content filtering for uploaded files is_safe, reason = content_filter.is_safe(file_content) if not is_safe: blocked_message = content_filter.get_blocked_message(reason) return jsonify({'error': blocked_message}), 400 # Process with appropriate agent based on file type if agent_type == 'mapping': agent = MappingAgent() elif agent_type == 'testcase': agent = TestCaseAgent() elif agent_type == 'sequence': agent = SequenceAgent() # Prepare input with file content and additional instructions agent_input = f"File: {filename}\n\nContent:\n{file_content}" if additional_instructions: agent_input += f"\n\nAdditional Instructions: {additional_instructions}" response = agent.process(agent_input) # Track file upload stats stats_manager.increment_files() # Store file upload output for potential JIRA posting (with user isolation) user_ip = request.remote_addr user_id = f"{user_ip}_file_upload" session_id = 'file_upload_' + str(hash(filename)) # Generate session ID for file uploads # Track user sessions for file uploads if user_id not in user_sessions: user_sessions[user_id] = [] if session_id not in user_sessions[user_id]: user_sessions[user_id].append(session_id) if session_id not in last_generated_output: last_generated_output[session_id] = {} # Determine correct type for JIRA posting if agent_type == 'mapping': content_type = 'mapping_document' elif agent_type == 'testcase': content_type = 'test_cases' elif agent_type == 'sequence': content_type = 'sequence_diagram' else: content_type = agent_type + 's' # Extract JSON data for CSV if it's a mapping document json_data = None if content_type == 'mapping_document': json_data = extract_json_from_response(response) last_generated_output[session_id] = { 'content': response, 'type': content_type, 'json_data': json_data } # Ask for CSV first for mapping documents, then JIRA if content_type == 'mapping_document': response += f"\n\nš Would you like to download this mapping as a CSV file?\n\n
" elif jira_client.is_enabled(): attachment_type = "PNG diagram" if content_type == 'sequence_diagram' and '```mermaid' in response else "Word document" response += f"\n\nš Would you like to post this as a {attachment_type} to a JIRA ticket?\n\n" return jsonify({'response': response}) except Exception as e: return jsonify({'error': str(e)}), 500 def handle_jira_request(user_message, session_id, user_id): import re # Extract JIRA ticket ID from message jira_pattern = r'([A-Z]+-\d+)' match = re.search(jira_pattern, user_message.upper()) if not match: return "Please provide a valid JIRA ticket ID (e.g., 'Post to JIRA ABC-123' or 'SCRUM-456')" ticket_id = match.group(1) # Check if we have recent output to post (only from current user's sessions) output_data = None user_session_list = user_sessions.get(user_id, []) for sid in user_session_list: if sid in last_generated_output: output_data = last_generated_output[sid] break if not output_data: return "No recent output found to post to JIRA. Please generate content first (mapping document, test cases, or sequence diagram)." # Validate ticket exists is_valid, validation_msg = jira_client.validate_ticket(ticket_id) if not is_valid: return f"JIRA Error: {validation_msg}" # Handle different content types try: if output_data['type'] == 'sequence_diagram' and '```mermaid' in output_data['content']: # Generate PNG for sequence diagrams png_path, png_filename = png_generator.generate_png_from_mermaid( output_data['content'], f"ArtifaX_{ticket_id}" ) if png_path: # Post PNG to JIRA success, result_msg = jira_client.post_with_attachment( ticket_id, output_data['content'], output_data['type'], png_path, png_filename ) # Clean up temp PNG file os.remove(png_path) else: return f"ā Failed to generate PNG: {png_filename}" elif output_data['type'] == 'mapping_document': # Generate CSV for mapping documents csv_path, csv_filename = csv_generator.generate_csv_from_mapping( output_data['content'], f"ArtifaX_{ticket_id}" ) if csv_path: # Post CSV to JIRA success, result_msg = jira_client.post_with_attachment( ticket_id, output_data['content'], output_data['type'], csv_path, csv_filename ) # Clean up temp CSV file os.remove(csv_path) else: return f"ā Failed to generate CSV: {csv_filename}" else: # Create Word document for other content types (test cases) doc_path, doc_filename = doc_generator.create_word_document( output_data['content'], output_data['type'], f"ArtifaX_{ticket_id}" ) # Post to JIRA with attachment success, result_msg = jira_client.post_with_attachment( ticket_id, output_data['content'], output_data['type'], doc_path, doc_filename ) # Clean up temp file os.remove(doc_path) except Exception as e: return f"ā Failed to create attachment: {str(e)}" if success: # Track JIRA upload stats stats_manager.increment_jira() return f"ā {result_msg}\n\nContent type: {output_data['type'].replace('_', ' ').title()}" else: return f"ā {result_msg}" @app.route('/chat', methods=['POST']) def chat(): user_message = request.json.get('message', '') session_id = request.json.get('session_id', 'default') user_ip = request.remote_addr # Create user identifier (IP + session prefix for isolation) user_id = f"{user_ip}_{session_id.split('_')[0]}" # Track user sessions if user_id not in user_sessions: user_sessions[user_id] = [] if session_id not in user_sessions[user_id]: user_sessions[user_id].append(session_id) # Debug logging print(f"DEBUG: User: {user_id}, Session: {session_id}, Message: '{user_message}'") print(f"DEBUG: User sessions: {user_sessions.get(user_id, [])}") # Content filtering is_safe, reason = content_filter.is_safe(user_message) if not is_safe: blocked_message = content_filter.get_blocked_message(reason) return jsonify({'response': blocked_message}) # Message length check if len(user_message) > content_filter.config['max_message_length']: return jsonify({'response': 'Message too long. Please keep messages under 10,000 characters.'}) # Initialize session if not exists if session_id not in chat_sessions: chat_sessions[session_id] = [] # Add user message to history chat_sessions[session_id].append({"role": "user", "content": user_message}) # Check if message contains file content if 'File uploaded:' in user_message and 'Content:' in user_message: # Extract file content for processing file_content = user_message.split('Content:\n', 1)[1] if 'Content:\n' in user_message else user_message try: # Direct agent calls without MCP # Get chat history for context (increased window for better continuity) chat_history = "\n".join([f"{msg['role']}: {msg['content']}" for msg in chat_sessions[session_id][-10:]]) # Last 10 messages context_message = f"Chat History:\n{chat_history}\n\nCurrent Message: {user_message}" # Determine which BA tool to use based on explicit requests if 'File uploaded:' in user_message: agent = MappingAgent() response = agent.process(context_message) elif ("generate" in user_message.lower() and "mapping" in user_message.lower()) or ("swagger" in user_message.lower() and "mapping" in user_message.lower()): # Only allow mapping via file upload agent = ConversationAgent() response = agent.process(context_message) if "upload" not in response.lower(): response = "I'd be happy to generate a mapping document for you! Please upload your Swagger/OpenAPI file (.json or .xml format) using the Upload button." elif ("generate" in user_message.lower() and "test case" in user_message.lower()) or ("frd" in user_message.lower() and "test" in user_message.lower()): # Only allow test case generation via file upload agent = ConversationAgent() response = agent.process(context_message) if "upload" not in response.lower(): response = "I'd be happy to generate test cases for you! Please upload your FRD document using the š attachment icon (.docx, .pdf, .txt formats supported)." elif ("generate" in user_message.lower() and "sequence" in user_message.lower()) or "sequence diagram" in user_message.lower() or "sequences" in user_message.lower() or "flowchart" in user_message.lower() or "graph" in user_message.lower() or ("sequence" in user_message.lower() and ("flow" in user_message.lower() or "diagram" in user_message.lower() or "process" in user_message.lower())): # Check if user provided sufficient details for sequence generation if len(user_message.split()) > 2 and any(word in user_message.lower() for word in ['for', 'with', 'between', 'process', 'flow', 'api', 'system']): agent = SequenceAgent() response = agent.process(context_message) # Clean up HTML entities in Mermaid output import html response = html.unescape(response) response = response.replace('>', '>').replace('<', '<').replace('&', '&') response = response.replace('->>', '->>') response = response.replace('-->>', '-->>') else: # Use conversation agent to understand context before suggesting agent = ConversationAgent() response = agent.process(context_message) if "more details" not in response.lower(): response = "I'd be happy to generate sequence diagrams for you! Please provide more details about the process flow you'd like to visualize.\n\nFor example:\n- 'Generate sequence diagram for user login process'\n- 'Create sequences for API integration between eStore and SAP'\n- 'Sequence diagram for payment processing flow'\n\nOr upload your process flow document using the š attachment icon." else: # Use conversation agent for general interactions agent = ConversationAgent() response = agent.process(context_message) # Store last generated output for potential JIRA posting (with user isolation) if any(keyword in user_message.lower() for keyword in ['generate', 'mapping', 'test case', 'sequence']) and not response.startswith("I'd be happy to") and '```mermaid' in response: last_generated_output[session_id] = { 'content': response, 'type': 'mapping_document' if 'mapping' in user_message.lower() else 'test_cases' if 'test' in user_message.lower() else 'sequence_diagram' } # Ensure session is tracked for this user if session_id not in user_sessions[user_id]: user_sessions[user_id].append(session_id) # Ask user if they want to post to JIRA (only for actual generated content) if jira_client.is_enabled() and not response.startswith("I'd be happy to"): attachment_type = "PNG diagram" if last_generated_output[session_id]['type'] == 'sequence_diagram' and '```mermaid' in response else "Word document" response += f"\n\nš Would you like to post this as a {attachment_type} to a JIRA ticket? If yes, please provide the JIRA ticket number (e.g., 'Post to JIRA ABC-123')." # Handle button clicks FIRST before other logic print(f"DEBUG: Checking button actions for message: '{user_message}'") print(f"DEBUG: Session ID: {session_id}") print(f"DEBUG: Available sessions: {list(last_generated_output.keys())}") if session_id in last_generated_output: print(f"DEBUG: Session content type: {last_generated_output[session_id]['type']}") if user_message == 'Download CSV': # Find mapping document ONLY in current user's sessions mapping_session = None user_session_list = user_sessions.get(user_id, []) for sid in user_session_list: if sid in last_generated_output and last_generated_output[sid]['type'] == 'mapping_document': mapping_session = sid break # Also check for file upload sessions from same user for sid, data in last_generated_output.items(): if (sid.startswith('file_upload_') and data['type'] == 'mapping_document' and sid not in user_session_list): user_sessions[user_id].append(sid) mapping_session = sid break if mapping_session: # Direct CSV conversion from stored content output_data = last_generated_output[mapping_session] csv_path, csv_filename = csv_generator.generate_csv_from_mapping( output_data['content'], f"mapping_{session_id}" ) if csv_path: with open(csv_path, 'r', encoding='utf-8') as f: csv_content = f.read() os.remove(csv_path) # Track CSV download stats stats_manager.increment_csv() csv_b64 = base64.b64encode(csv_content.encode('utf-8')).decode('utf-8') response = f"š CSV file generated!\n\nš„ Download CSV\n\nš Would you like to upload this mapping document to JIRA?\n\n" else: response = f"ā Failed to generate CSV: {csv_filename}" else: response = "No mapping document found. Please generate a mapping document first." elif user_message == 'No CSV': # User declined CSV, offer JIRA if jira_client.is_enabled(): response = "š Would you like to upload this mapping document to JIRA?\n\n" else: response = "No problem! Your mapping document is ready above. You can copy it or generate a new one anytime." elif user_message == 'Post to JIRA': response = "Please provide the JIRA ticket number (e.g., 'ABC-123' or 'SCRUM-456'):" elif user_message == 'Upload to JIRA': response = "Please provide the JIRA ticket number (e.g., 'ABC-123' or 'SCRUM-456'):" elif user_message == 'No JIRA' or user_message == 'No JIRA Upload': response = "Perfect! Your document is ready above. You can copy it or generate a new one anytime." # Check for JIRA ticket number input else: jira_pattern = r'([A-Z]+-\d+)' if re.search(jira_pattern, user_message.upper()): jira_response = handle_jira_request(user_message, session_id, user_id) response = jira_response # Aggressive HTML entity cleanup for Mermaid diagrams if '```mermaid' in response: import html # Multiple passes to handle nested encoding for _ in range(3): response = html.unescape(response) response = response.replace('>>', '>>') response = response.replace('>', '>') response = response.replace('>>', '>>') response = response.replace('>', '>') response = response.replace('<', '<') response = response.replace('&', '&') response = response.replace(''', "'") response = response.replace('"', '"') # Fix arrow syntax specifically response = response.replace('-->', '-->') response = response.replace('->', '->') response = response.replace('->>', '->') response = response.replace('-->>', '-->') # Remove trailing backslashes response = response.replace('\\', '') # Fix broken alt syntax response = response.replace('alt After some time -->>| timeout |', 'alt After some time') response = response.replace('-->>|', '-->') # Add missing end statement if 'alt ' in response and response.count('end') < response.count('alt '): response = response.replace('deactivate IIB', 'end\n deactivate IIB') # Add bot response to history chat_sessions[session_id].append({"role": "assistant", "content": response}) # Log chat interaction chat_logger.log_chat(session_id, user_message, response, user_ip) # Track stats stats_manager.increment_messages() return jsonify({"response": response}) except Exception as e: return jsonify({"response": f"Error: {str(e)}"}) @app.route('/logs/stats', methods=['GET']) def get_logs_stats(): """Get usage statistics""" stats = chat_logger.get_usage_stats() return jsonify(stats) @app.route('/stats', methods=['GET']) def get_stats(): """Get dashboard statistics""" return jsonify(stats_manager.get_stats()) @app.route('/logs/push', methods=['POST']) def push_logs(): """Manually push logs to GitHub""" success = chat_logger.push_logs_to_github() return jsonify({'success': success}) @app.route('/sync/github', methods=['POST']) def sync_to_github(): """Sync files modified since last sync to GitHub""" from github_manager import GitHubManager import time from datetime import datetime LAST_SYNC_FILE = '.last_sync' def get_last_sync_time(): if os.path.exists(LAST_SYNC_FILE): try: with open(LAST_SYNC_FILE, 'r') as f: data = json.load(f) return data.get('timestamp', 0) except: pass return 0 def save_last_sync_time(): with open(LAST_SYNC_FILE, 'w') as f: json.dump({ 'timestamp': time.time(), 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S') }, f, indent=2) github_manager = GitHubManager() files_synced = [] # Get last sync timestamp last_sync = get_last_sync_time() # Define file extensions to track tracked_extensions = {'.py', '.js', '.css', '.html', '.md', '.json', '.txt'} # Scan for files modified since last sync success_count = 0 for root, dirs, files in os.walk('.'): # Skip certain directories dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['__pycache__', 'temp', 'uploads', 'logs']] for file in files: file_path = os.path.join(root, file).replace('\\', '/').lstrip('./') # Check if file extension is tracked if any(file.endswith(ext) for ext in tracked_extensions): try: mod_time = os.path.getmtime(os.path.join(root, file)) if mod_time > last_sync: # File was modified since last sync, sync it try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() repo = github_manager.github.get_user().get_repo('ArtifaX') try: # Try to update existing file on AIIntOps branch file_obj = repo.get_contents(file_path, ref="AIIntOps") repo.update_file(file_path, f"AIIntOps: Update {file_path}", content, file_obj.sha, branch="AIIntOps") success = True except Exception as e: if 'Not Found' in str(e): # File doesn't exist, create it on AIIntOps branch repo.create_file(file_path, f"AIIntOps: Add {file_path}", content, branch="AIIntOps") success = True else: success = False if success: success_count += 1 files_synced.append({ 'file': file_path, 'success': success, 'modified': datetime.fromtimestamp(mod_time).strftime('%Y-%m-%d %H:%M:%S') }) except Exception as e: files_synced.append({ 'file': file_path, 'success': False, 'error': str(e), 'modified': datetime.fromtimestamp(mod_time).strftime('%Y-%m-%d %H:%M:%S') }) except OSError: continue # Save sync timestamp only if at least one file was successfully synced if success_count > 0: save_last_sync_time() return jsonify({ 'files_synced': files_synced, 'last_sync': datetime.fromtimestamp(last_sync).strftime('%Y-%m-%d %H:%M:%S') if last_sync > 0 else 'Never', 'total_files': len(files_synced), 'successful_syncs': success_count }) @app.route('/feedback', methods=['POST']) def submit_feedback(): try: feedback_data = request.json success, message = email_sender.send_feedback(feedback_data) return jsonify({'success': success, 'message': message}) except Exception as e: return jsonify({'success': False, 'message': str(e)}) @app.route('/jira/config', methods=['GET', 'POST']) def jira_config(): global jira_client if request.method == 'GET': config = jira_client.config.copy() # Don't expose sensitive data config['api_token'] = '***' if config['api_token'] else '' return jsonify(config) elif request.method == 'POST': # Update JIRA configuration new_config = request.json config_path = os.path.join('config', 'jira_config.json') # Update config file with open(config_path, 'w') as f: json.dump(new_config, f, indent=2) # Reinitialize client jira_client = JIRAClient() return jsonify({'status': 'JIRA configuration updated'}) if __name__ == '__main__': socketio.run(app, debug=False, host='0.0.0.0', port=8080)