Files
patchy631--ai-engineering-hub/paralegal-agent-crew/app.py
T
2026-07-13 12:37:47 +08:00

444 lines
17 KiB
Python

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"""<iframe src="data:application/pdf;base64,{base64_pdf}" width="400" height="100%" type="application/pdf"
style="height:100vh; width:100%"
>
</iframe>"""
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"""
<div style="font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; white-space: pre-wrap; line-height: 1.45; font-size: 13px;">
{html_body}
</div>
""",
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('''
<h1 style='color: #2E86AB; margin-bottom: 10px;'>
⚖️ Paralegal AI assistant
</h1>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 20px;">
<span style='color: #A23B72; font-size: 16px;'>Powered by</span>
<div style="display: flex; align-items: center; gap: 20px;">
<a href="#" style="display: inline-block; vertical-align: middle;">
<img src="https://images.seeklogo.com/logo-png/61/2/crew-ai-logo-png_seeklogo-619843.png"
alt="CrewAI" style="height: 100px;">
</a>
<a href="#" style="display: inline-block; vertical-align: middle;">
<img src="https://milvus.io/images/layout/milvus-logo.svg"
alt="Milvus" style="height: 32px;">
</a>
<a href="#" style="display: inline-block; vertical-align: middle;">
<img src="https://i.ibb.co/VcsfddTr/logo-dark.png"
alt="Firecrawl" style="height: 45px;">
</a>
<a href="#" style="display: inline-block; vertical-align: middle;">
<img src="https://i.ibb.co/wt57zN1/ollama.png"
alt="Ollama" style="height: 48px;">
</a>
</div>
</div>
''', 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(
"<p style='text-align: center; color: #666; font-size: 12px;'>"
"Paralegal AI assistant • Built with Streamlit, CrewAI, Milvus, Firecrawl, and Ollama"
"</p>",
unsafe_allow_html=True
)