import nest_asyncio nest_asyncio.apply() import os import asyncio import streamlit as st import base64 import gc import tempfile import uuid import time import io import re from contextlib import redirect_stdout from pathlib import Path from src.embeddings.embed_data import EmbedData from src.indexing.milvus_vdb import MilvusVDB from src.retrieval.retriever_rerank import Retriever from src.generation.rag import RAG from src.workflows.agent_workflow import ParalegalAgentWorkflow from pypdf import PdfReader from dotenv import load_dotenv from config.settings import settings # Load environment variables load_dotenv() # Set up page configuration st.set_page_config(page_title="Paralegal AI Assistant", layout="wide") # Initialize session state variables if "id" not in st.session_state: st.session_state.id = str(uuid.uuid4())[:8] st.session_state.file_cache = {} if "workflow" not in st.session_state: st.session_state.workflow = None if "messages" not in st.session_state: st.session_state.messages = [] if "workflow_logs" not in st.session_state: st.session_state.workflow_logs = [] if "vector_db" not in st.session_state: st.session_state.vector_db = None session_id = st.session_state.id def reset_chat(): """Reset chat history and clear memory.""" st.session_state.messages = [] st.session_state.workflow_logs = [] gc.collect() def display_pdf(file): """Display PDF preview in sidebar.""" st.markdown("### PDF Preview") base64_pdf = base64.b64encode(file.read()).decode("utf-8") pdf_display = f"""""" st.markdown(pdf_display, unsafe_allow_html=True) def render_logs(log_text: str): """Render logs with ANSI colors and emojis nicely in Streamlit""" from ansi2html import Ansi2HTMLConverter conv = Ansi2HTMLConverter(inline=True) html_body = conv.convert(log_text, full=False) st.markdown( f"""
{html_body}
""", unsafe_allow_html=True, ) def load_and_split_pdf(file_path: str, chunk_size: int = 512, chunk_overlap: int = 50): try: reader = PdfReader(file_path) full_text_parts = [] for page in reader.pages: text = page.extract_text() or "" if text: full_text_parts.append(text) full_text = "\n".join(full_text_parts) words = full_text.split() chunks = [] i = 0 step = max(1, chunk_size - chunk_overlap) while i < len(words): segment = words[i : i + chunk_size] chunks.append(" ".join(segment)) i += step return [c for c in chunks if c.strip()] except Exception as e: st.error(f"Error loading PDF: {e}") return [] def initialize_workflow(file_path: str): with st.spinner("🔄 Loading document and setting up the workflow..."): try: # Step 1: Load and split document st.info("📄 Loading and processing PDF...") text_chunks = load_and_split_pdf(file_path) if not text_chunks: st.error("No text chunks extracted from PDF") return None st.success(f"✅ Created {len(text_chunks)} text chunks") # Step 2: Create embeddings st.info("🧠 Generating embeddings...") embed_data = EmbedData( embed_model_name=settings.embedding_model, batch_size=settings.batch_size ) embed_data.embed(text_chunks) st.success("✅ Embeddings generated with binary quantization") # Step 3: Setup vector database st.info("🗄️ Setting up Milvus vector database...") collection_name = f"{settings.collection_name}_{session_id}" vector_db = MilvusVDB( collection_name=collection_name, vector_dim=settings.vector_dim, batch_size=settings.batch_size, db_file=f"{settings.milvus_db_path}_{session_id}.db" ) vector_db.initialize_client() vector_db.create_collection() vector_db.ingest_data(embed_data) # Store in session state for cleanup st.session_state.vector_db = vector_db st.success("✅ Vector database setup completed") # Step 4: Setup retrieval st.info("🔍 Setting up retrieval system...") retriever = Retriever( vector_db=vector_db, embed_data=embed_data, top_k=settings.top_k ) st.success("✅ Retrieval system ready") # Step 5: Setup RAG system st.info("🤖 Setting up RAG system...") rag_system = RAG( retriever=retriever, llm_model=settings.llm_model, temperature=settings.temperature, max_tokens=settings.max_tokens ) st.success("✅ RAG system initialized") # Step 6: Setup workflow st.info("⚙️ Setting up agentic workflow...") workflow = ParalegalAgentWorkflow( retriever=retriever, rag_system=rag_system, firecrawl_api_key=settings.firecrawl_api_key or os.getenv("FIRECRAWL_API_KEY"), openai_api_key=settings.openai_api_key or os.getenv("OPENAI_API_KEY") ) st.success("🎉 Workflow setup completed!") return workflow except Exception as e: st.error(f"Error initializing workflow: {e}") return None async def run_workflow(query: str): f = io.StringIO() with redirect_stdout(f): result = await st.session_state.workflow.run_workflow(query) # Get aptured logs and store them logs = f.getvalue() if logs: st.session_state.workflow_logs.append(logs) return result def cleanup_resources(): """Cleanup vector database and other resources.""" if st.session_state.vector_db: try: st.session_state.vector_db.close() except: pass st.session_state.vector_db = None # Sidebar for configuration and document upload with st.sidebar: st.header("🔧 Configuration") # st.subheader("API Keys") # openai_key = st.text_input("OpenAI API Key", type="password", value=os.getenv("OPENAI_API_KEY", "")) ollama_model = st.text_input("Ollama Model", value="gpt-oss:20b") firecrawl_key = st.text_input("Firecrawl API Key", type="password", value=os.getenv("FIRECRAWL_API_KEY", "")) # if openai_key: # os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") # st.success("✅ OpenAI API Key set!") os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") if firecrawl_key: os.environ["FIRECRAWL_API_KEY"] = firecrawl_key st.success("✅ Firecrawl API Key set!") st.markdown("---") # Document upload section st.header("📄 Upload Document") st.markdown("Upload a PDF document to get started") uploaded_file = st.file_uploader("Choose your PDF file", type="pdf") if uploaded_file: try: with tempfile.TemporaryDirectory() as temp_dir: file_path = os.path.join(temp_dir, uploaded_file.name) with open(file_path, "wb") as f: f.write(uploaded_file.getvalue()) file_key = f"{session_id}-{uploaded_file.name}" if file_key not in st.session_state.get('file_cache', {}): # Initialize workflow with the uploaded document workflow = initialize_workflow(file_path) if workflow: st.session_state.workflow = workflow st.session_state.file_cache[file_key] = workflow st.balloons() else: st.session_state.workflow = st.session_state.file_cache[file_key] if st.session_state.workflow: st.success("🎉 Ready to Chat!") display_pdf(uploaded_file) except Exception as e: st.error(f"An error occurred: {e}") # elif uploaded_file and not openai_key: # st.warning("⚠️ Please enter your OpenAI API key first!") elif uploaded_file: st.info("📁 Please upload a PDF to continue") # Cleanup button st.markdown("---") if st.button("🗑️ Clean Up Resources"): cleanup_resources() st.success("Resources cleaned up!") # Main chat interface col1, col2 = st.columns([6, 1]) with col1: st.markdown('''

⚖️ Paralegal AI assistant

Powered by
CrewAI Milvus Firecrawl Ollama
''', unsafe_allow_html=True) with col2: if st.button("Clear Chat ↺", on_click=reset_chat): st.rerun() # System info if st.session_state.workflow: st.success("🟢 System Ready - Workflow initialized successfully!") else: st.info("🔵 Upload a PDF document to get started") # Display chat messages from history for i, message in enumerate(st.session_state.messages): with st.chat_message(message["role"]): st.markdown(message["content"]) # # Display workflow logs for user messages # if (message["role"] == "user" and # "log_index" in message and # message["log_index"] < len(st.session_state.workflow_logs)): # with st.expander("🔍 View Workflow Execution Details", expanded=False): # logs = st.session_state.workflow_logs[message["log_index"]] # render_logs(logs) # Accept user input if prompt := st.chat_input("Ask a question about your document..."): if not st.session_state.workflow: st.error("⚠️ Please upload a document first to initialize the workflow.") st.stop() if not os.getenv("OPENAI_API_KEY"): st.error("⚠️ Please set your OpenAI API key in the sidebar.") st.stop() # Add user message to chat history log_index = len(st.session_state.workflow_logs) st.session_state.messages.append({ "role": "user", "content": prompt, "log_index": log_index }) # Display user message with st.chat_message("user"): st.markdown(prompt) # Run the workflow and get response with st.chat_message("assistant"): message_placeholder = st.empty() try: with st.spinner("🔄 Processing your query..."): # Measure end-to-end workflow time workflow_start = time.perf_counter() result = asyncio.run(run_workflow(prompt)) workflow_end = time.perf_counter() workflow_time = workflow_end - workflow_start # # Display workflow logs # if log_index < len(st.session_state.workflow_logs): # with st.expander("🔍 View Workflow Execution Details", expanded=False): # render_logs(st.session_state.workflow_logs[log_index]) # Get the final answer if isinstance(result, dict) and "answer" in result: full_response = result["answer"] # Show additional info about the workflow if result.get("web_search_used", False): st.info("🌐 This response includes information from web search") # if 'workflow_time' in locals(): # st.caption(f"🕒 Completion time: {workflow_time:.2f} s") else: st.info("📚 This response is based on your document") try: retriever = getattr(st.session_state.workflow, "retriever", None) if retriever: retrieve_start = time.perf_counter() retriever.search(prompt) retrieve_end = time.perf_counter() retrieval_time = retrieve_end - retrieve_start citations = retriever.get_citations(prompt, top_k=settings.top_k, snippet_chars=300) if citations: with st.expander("📎 Citations (top matches)"): for c in citations: score = c.get("score") try: score_str = f"{float(score):.3f}" except Exception: score_str = str(score) st.markdown( f"[{c['rank']}] score={score_str} id={c.get('node_id')}" ) if c.get("snippet"): st.code(c["snippet"], language="text") except Exception as e: st.warning(f"Could not fetch citations: {e}") # Show timing caption times = [] if retrieval_time is not None: times.append(f"🕒 Retrieval time: {retrieval_time:.2f} s") # if 'workflow_time' in locals(): # times.append(f"🕒 Completion time: {workflow_time:.2f} s") if times: st.caption(" • ".join(times)) else: full_response = str(result) # Stream the response word by word streamed_response = "" words = full_response.split() for i, word in enumerate(words): streamed_response += word + " " message_placeholder.markdown(streamed_response + "▌") if i < len(words) - 1: time.sleep(0.05) # Display final response message_placeholder.markdown(full_response) except Exception as e: error_msg = f"❌ Error processing your question: {str(e)}" st.error(error_msg) full_response = "I apologize, but I encountered an error while processing your question. Please try again." message_placeholder.markdown(full_response) # Add assistant response to chat history st.session_state.messages.append({ "role": "assistant", "content": full_response }) # Footer st.markdown("---") st.markdown( "

" "Paralegal AI assistant • Built with Streamlit, CrewAI, Milvus, Firecrawl, and Ollama" "

", unsafe_allow_html=True )