import os import tempfile import requests from typing import Any, Dict import streamlit as st from groundx_utils import ( create_client, ensure_bucket, check_file_exists, get_xray_for_existing_document, process_document ) # Application Configuration st.set_page_config(page_title="Ground X - X-Ray", layout="wide") # Custom CSS for enhanced chat interface layout st.markdown(""" """, unsafe_allow_html=True) def reset_analysis(): """Reset all analysis-related session state variables""" keys_to_delete = ["xray_data", "uploaded_file_path", "uploaded_file_name", "uploaded_file_type", "processing_complete", "used_existing_file", "auto_loaded_file", "chat_history"] for key in keys_to_delete: if key in st.session_state: del st.session_state[key] # Chat Interface Functions def prepare_chat_context(xray_data, prompt): """Prepare context from X-Ray data for the LLM""" context_parts = [] # Add summary first for quick overview if xray_data.get('fileSummary'): context_parts.append(f"Summary: {xray_data['fileSummary']}") # Add limited document content (first 2 pages, first 3 chunks each) if 'documentPages' in xray_data and xray_data['documentPages']: extracted_texts = [] for page in xray_data['documentPages'][:2]: # Only first 2 pages if 'chunks' in page: for chunk in page['chunks'][:3]: # Only first 3 chunks per page if 'text' in chunk and chunk['text']: text = chunk['text'] if len(text) > 500: # Shorter limit text = text[:500] + "..." extracted_texts.append(text) if extracted_texts: context_parts.append(f"Document Content: {' '.join(extracted_texts)}") # Add essential metadata if xray_data.get('fileType'): context_parts.append(f"File Type: {xray_data['fileType']}") if xray_data.get('language'): context_parts.append(f"Language: {xray_data['language']}") return "\n\n".join(context_parts) def generate_chat_response(prompt, context): """Generate AI response using Ollama with structured prompt engineering""" # Construct comprehensive prompt for intelligent query handling full_prompt = f"""You are an AI assistant helping analyze a document. You have access to the following document information: {context} User Question: {prompt} Instructions: - Answer the question directly and concisely - Use Document Content for specific details - Use Summary for general overview - Don't add unnecessary disclaimers or explanations - If you don't have the information, simply say "I don't have enough information to answer that question" - Keep responses focused and to the point Response:""" # Initialize Ollama API request try: response = requests.post( "http://localhost:11434/api/generate", json={ "model": "phi3:mini", "prompt": full_prompt, "stream": False, "options": { "temperature": 0.3, "top_p": 0.9, "num_predict": 300, "top_k": 10, "repeat_penalty": 1.1 } }, timeout=60 ) if response.status_code == 200: result = response.json() return result.get("response", "Sorry, I couldn't generate a response.") else: return f"I'm having trouble accessing the AI model right now. Status: {response.status_code}" except Exception as e: return f"I'm having trouble accessing the AI model right now. Error: {str(e)}" # Initialize Streamlit session state for key in ["xray_data", "uploaded_file_path", "uploaded_file_name", "uploaded_file_type", "processing_complete", "used_existing_file", "auto_loaded_file"]: if key not in st.session_state: st.session_state[key] = None if key == "xray_data" else False # Application Header st.markdown(""" # World-class Document Processing Pipeline """) # Load and display GroundX branding import base64 try: with open("assets/groundx.png", "rb") as img_file: logo_base64 = base64.b64encode(img_file.read()).decode() st.markdown(f"""
powered by

""", unsafe_allow_html=True) except FileNotFoundError: st.markdown("""
powered by Ground X

""", unsafe_allow_html=True) # Document Upload Interface with st.sidebar: st.header("📄 Upload Document") if st.session_state.uploaded_file_name: status_text = f"✅ **File loaded**: {st.session_state.uploaded_file_name}" if st.session_state.used_existing_file: status_text += " (Auto-loaded from bucket)" if st.session_state.auto_loaded_file else " (Used existing file in bucket)" st.success(status_text) if st.button("🔄 Re-process File"): st.session_state.processing_complete = False st.session_state.xray_data = None st.session_state.used_existing_file = False st.session_state.auto_loaded_file = False st.rerun() uploaded = st.file_uploader( "Choose a document", type=["pdf", "png", "jpg", "jpeg", "docx"], help="Upload a document to analyze with Ground X X-Ray" ) if uploaded is not None: st.info(f"**File**: {uploaded.name}\n**Size**: {uploaded.size / 1024:.1f} KB\n**Type**: {uploaded.type}") st.session_state.uploaded_file_name = uploaded.name st.session_state.uploaded_file_type = uploaded.type st.button("🔄 Clear Analysis", on_click=reset_analysis) # Initialize Ground X API client and storage bucket try: gx = create_client() bucket_id = ensure_bucket(gx) except ValueError as e: st.error(f"❌ {e}") st.stop() # Auto-load existing document from bucket if available if not st.session_state.auto_loaded_file and not st.session_state.xray_data: # Configurable sample file - can be set via environment variable existing_file_name = os.getenv("SAMPLE_FILE_NAME", "tmpivkf8qf8_sample-file.pdf") existing_doc_id = check_file_exists(gx, bucket_id, existing_file_name) if existing_doc_id: xray = get_xray_for_existing_document(gx, existing_doc_id, bucket_id) st.session_state.xray_data = xray st.session_state.uploaded_file_name = existing_file_name st.session_state.uploaded_file_type = "application/pdf" st.session_state.processing_complete = True st.session_state.used_existing_file = True st.session_state.auto_loaded_file = True st.success(f"✅ **Auto-loaded**: {existing_file_name} from bucket") st.rerun() else: st.session_state.auto_loaded_file = True # Document Processing Logic should_process = False file_to_process = None if uploaded is not None: should_process = True file_to_process = uploaded tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=f"_{uploaded.name}") tmp_file.write(uploaded.getbuffer()) tmp_file.close() st.session_state.uploaded_file_path = tmp_file.name elif st.session_state.uploaded_file_path and not st.session_state.processing_complete: should_process = True class MockUploadedFile: def __init__(self, name, type, path): self.name = name self.type = type self.path = path def getbuffer(self): with open(self.path, 'rb') as f: return f.read() file_to_process = MockUploadedFile( st.session_state.uploaded_file_name, st.session_state.uploaded_file_type, st.session_state.uploaded_file_path ) if should_process and st.session_state.xray_data is None: with st.status("🔄 Processing document...", expanded=True) as status: # Determine file path for processing (temporary for new uploads, stored for existing) if hasattr(file_to_process, 'path'): file_path = file_to_process.path else: file_path = st.session_state.uploaded_file_path try: xray, used_existing = process_document(gx, bucket_id, file_to_process, file_path) st.session_state.xray_data = xray st.session_state.processing_complete = True st.session_state.used_existing_file = used_existing if used_existing: st.write("✅ **File already exists in bucket**") st.write("📥 **Fetched X-Ray data...**") st.success("✅ Document analysis completed! (Used existing file)") else: st.write("📤 **Uploaded to Ground X...**") st.write("⏳ **Processed document...**") st.write("📥 **Fetched X-Ray data...**") st.success("✅ Document parsed successfully! Explore the results below.") st.write("🎉 **Analysis complete!**") except Exception as e: st.error(f"❌ Error processing document: {str(e)}") st.session_state.processing_complete = False # Analysis Results Display if st.session_state.xray_data: xray = st.session_state.xray_data # Extract and display document metadata metrics file_type = xray.get('fileType', 'Unknown') language = xray.get('language', 'Unknown') pages = len(xray.get("documentPages", [])) keywords = len(xray.get("fileKeywords", "").split(",")) if xray.get("fileKeywords") else 0 st.markdown(f"**File Type:** {file_type}              **Language:** {language}              **Pages:** {pages}              **Keywords:** {keywords}") # Primary interface tabs for analysis and interaction main_tabs = st.tabs([ "📊 X-Ray Analysis", "💬 Chat" ]) with main_tabs[0]: st.markdown("### 📊 X-Ray Analysis Results") tabs = st.tabs([ "🔍 JSON Output", "📝 Narrative Summary", "📋 File Summary", "💡 Suggested Text", "📄 Extracted Text", "🏷️ Keywords" ]) with tabs[0]: st.subheader("🔍 Raw JSON Data") st.json(xray) with tabs[1]: st.subheader("📝 Narrative Summary") # Extract and display narrative content from document chunks narratives = [] if "documentPages" in xray: for page in xray["documentPages"]: if "chunks" in page: for chunk in page["chunks"]: if "narrative" in chunk and chunk["narrative"]: narratives.extend(chunk["narrative"]) if narratives: for i, narrative in enumerate(narratives, 1): st.markdown(f"**Narrative {i}:**") st.markdown(narrative) st.divider() else: st.info("No narrative text found in the X-Ray data") with tabs[2]: st.subheader("📋 File Summary") file_summary = xray.get("fileSummary") if file_summary: st.markdown(file_summary) else: st.info("No file summary found in the X-Ray data") with tabs[3]: st.subheader("💡 Suggested Text") # Extract and display suggested text content from document chunks suggested_texts = [] if "documentPages" in xray: for page in xray["documentPages"]: if "chunks" in page: for chunk in page["chunks"]: if "suggestedText" in chunk and chunk["suggestedText"]: suggested_texts.append(chunk["suggestedText"]) if suggested_texts: for i, suggested in enumerate(suggested_texts, 1): st.markdown(f"**Suggested Text {i}:**") st.markdown(suggested) st.divider() else: st.info("No suggested text found in the X-Ray data") with tabs[4]: st.subheader("📄 Extracted Text") # Extract and display raw text content from document chunks extracted_texts = [] if "documentPages" in xray: for page in xray["documentPages"]: if "chunks" in page: for chunk in page["chunks"]: if "text" in chunk and chunk["text"]: extracted_texts.append(chunk["text"]) if extracted_texts: combined_text = "\n\n---\n\n".join(extracted_texts) st.text_area("Extracted Content", combined_text, height=400) else: st.info("No extracted text found in the X-Ray data") with tabs[5]: st.subheader("🏷️ Keywords") keywords = xray.get("fileKeywords") if keywords: st.write(keywords) else: st.info("No keywords found in the X-Ray data") # Interactive Chat Interface with main_tabs[1]: st.markdown("### 💬 Chat with Document") st.markdown("Ask questions about your document.") # Initialize and display chat conversation history if "chat_history" not in st.session_state: st.session_state.chat_history = [] # Render existing chat messages for message in st.session_state.chat_history: with st.chat_message(message["role"]): st.markdown(message["content"]) # Process user input and generate responses if prompt := st.chat_input("Ask a question about your document..."): # Store user message in conversation history st.session_state.chat_history.append({"role": "user", "content": prompt}) # Display user message in chat interface with st.chat_message("user"): st.markdown(prompt) # Generate and display AI assistant response with st.chat_message("assistant"): with st.spinner("Thinking..."): context = prepare_chat_context(xray, prompt) response = generate_chat_response(prompt, context) st.session_state.chat_history.append({"role": "assistant", "content": response}) st.markdown(response) elif uploaded is None and st.session_state.uploaded_file_name is None and not st.session_state.xray_data: st.info("👆 **Upload a document in the sidebar to begin analysis**") elif uploaded is None and st.session_state.uploaded_file_name and st.session_state.xray_data: status = "Auto-loaded" if st.session_state.auto_loaded_file else "Analysis" st.success(f"✅ **{status} complete for**: {st.session_state.uploaded_file_name}") st.info("💡 **Tip**: You can re-process the file using the button in the sidebar, or upload a new document.")