ArtifaXChatBot / web_server.py
dharu007's picture
Update web_server.py
33f98e8 verified
Raw History Blame Contribute Delete
30.8 kB
#!/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<div class='option-buttons'><button class='option-btn' onclick='handleOptionClick(\"Download CSV\")'>πŸ“₯ Download CSV</button><button class='option-btn secondary' onclick='handleOptionClick(\"No CSV\")'>❌ No Thanks</button></div>"
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<div class='option-buttons'><button class='option-btn' onclick='handleOptionClick(\"Post to JIRA\")'>πŸ“‹ Post to JIRA</button><button class='option-btn secondary' onclick='handleOptionClick(\"No JIRA\")'>❌ No Thanks</button></div>"
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('&gt;', '>').replace('&lt;', '<').replace('&amp;', '&')
response = response.replace('-&gt;&gt;', '->>')
response = response.replace('--&gt;&gt;', '-->>')
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<a href='data:text/csv;base64,{csv_b64}' download='{csv_filename}' style='background: #28a745; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block; margin: 10px 0;'>πŸ“₯ Download CSV</a>\n\nπŸ“‹ Would you like to upload this mapping document to JIRA?\n\n<div class='option-buttons'><button class='option-btn' onclick='handleOptionClick(\"Upload to JIRA\")'>πŸ“‹ Yes, Upload to JIRA</button><button class='option-btn secondary' onclick='handleOptionClick(\"No JIRA Upload\")'>❌ No Thanks</button></div>"
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<div class='option-buttons'><button class='option-btn' onclick='handleOptionClick(\"Upload to JIRA\")'>πŸ“‹ Yes, Upload to JIRA</button><button class='option-btn secondary' onclick='handleOptionClick(\"No JIRA Upload\")'>❌ No Thanks</button></div>"
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('&amp;gt;&gt;', '>>')
response = response.replace('&amp;gt;', '>')
response = response.replace('&gt;&gt;', '>>')
response = response.replace('&gt;', '>')
response = response.replace('&lt;', '<')
response = response.replace('&amp;', '&')
response = response.replace('&#39;', "'")
response = response.replace('&quot;', '"')
# Fix arrow syntax specifically
response = response.replace('--&gt;', '-->')
response = response.replace('-&gt;', '->')
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)