| import os |
| import uuid |
| import tempfile |
| from typing import Annotated, List, TypedDict, Dict, Any |
|
|
| import streamlit as st |
|
|
| from langchain_core.documents import Document |
| from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage |
| from langchain_core.tools import tool |
| from langchain_openai import ChatOpenAI, OpenAIEmbeddings |
| from langchain_text_splitters import RecursiveCharacterTextSplitter |
|
|
| try: |
| from langchain_chroma import Chroma |
| except Exception: |
| from langchain_community.vectorstores import Chroma |
|
|
| try: |
| from langchain_community.tools.tavily_search import TavilySearchResults |
| except Exception: |
| TavilySearchResults = None |
|
|
| from langgraph.graph import StateGraph, END |
| from langgraph.graph.message import add_messages |
| from langgraph.prebuilt import ToolNode |
| from langgraph.checkpoint.memory import MemorySaver |
|
|
|
|
| st.set_page_config( |
| page_title="Legal Agentic AI Project", |
| page_icon="βοΈ", |
| layout="wide", |
| ) |
|
|
|
|
| SAMPLE_CASE = ''' |
| Case No: 2024/CR/12345 |
| Court: District Court, US |
| |
| Facts: |
| The defendant, Mr. Alex Carter, was charged with breach of contract dated January 15, 2023. |
| The plaintiff, ABC Corporation, claims that the defendant failed to deliver 1000 units of |
| electronic components as per the signed agreement. The contract specified delivery by March 31, 2023. |
| |
| The defendant claims that due to unforeseen supply chain disruptions caused by the pandemic, |
| delivery was impossible. However, no force majeure clause was explicitly invoked in writing |
| within the stipulated 30-day notice period as mentioned in Section 12 of the contract. |
| |
| The plaintiff seeks damages of Rs. 50,00,000 for business losses incurred due to delayed delivery. |
| The defendant argues that the damages claimed are excessive and not proven with adequate documentation. |
| |
| Key Issues: |
| 1. Was there a valid force majeure event? |
| 2. Did the defendant provide proper notice? |
| 3. Are the damages claimed reasonable and substantiated? |
| 4. Was there any contributory negligence by the plaintiff? |
| ''' |
|
|
|
|
| LEGAL_KNOWLEDGE_BASE = [ |
| ''' |
| Precedent Case: Smith v. Johnson (2022) |
| Court ruled that force majeure must be invoked in writing within the contractual notice period. |
| Failure to do so waives the right to claim force majeure defense. |
| Citation: [2022] 3 SCC 145 |
| ''', |
| ''' |
| Precedent Case: ABC Corp v. XYZ Ltd (2021) |
| In breach of contract cases, damages must be proven with documentary evidence. |
| Speculative damages without supporting documentation are not admissible. |
| The burden of proof lies with the plaintiff. |
| Citation: [2021] 2 BomHC 78 |
| ''', |
| ''' |
| Legal Principle: Contributory Negligence |
| If the plaintiff's actions contributed to the breach or losses, damages may be reduced proportionally. |
| Courts have recognized this principle in commercial contract disputes. |
| Reference: US Contract Act, 1872, Section 73 |
| ''', |
| ''' |
| Force Majeure During Pandemic |
| Courts have taken a nuanced view of COVID-19 as force majeure. |
| The invoking party must demonstrate: (1) impossibility, not mere difficulty, |
| (2) proper notice, and (3) mitigation efforts. |
| Citation: Energy Watchdog v. CERC (2020) |
| ''', |
| ''' |
| Force Majeure Invocation - Notice Requirement |
| Relevant Precedent: Smith v. Johnson (2022) |
| Citation: [2022] 3 SCC 145 |
| Legal Principle: |
| Force majeure must be invoked in writing within the contractual notice period. |
| Failure to comply with the notice requirement waives the right to claim force majeure defense. |
| |
| Application to Current Case: |
| The contract specified a 30-day notice period under Section 12. |
| Mr. Alex Carter failed to invoke force majeure in writing within this stipulated period. |
| This precedent strongly supports the plaintiff's position. |
| ''', |
| ''' |
| Burden of Proof for Damages |
| Relevant Precedent: ABC Corp v. XYZ Ltd (2021) |
| Citation: [2021] 2 BomHC 78 |
| Legal Principle: |
| In breach of contract cases, damages must be proven with documentary evidence. |
| Speculative damages without supporting documentation are not admissible. |
| The burden of proof lies with the plaintiff. |
| |
| Application to Current Case: |
| ABC Corporation claims Rs. 50,00,000 in damages. |
| The plaintiff must provide concrete evidence such as lost orders, financial statements, |
| alternative procurement costs, and documented business loss. |
| ''', |
| ''' |
| Relevant Precedent: Energy Watchdog v. CERC (2020) |
| Legal Principle: |
| Courts apply a nuanced, three-part test for COVID-19 related force majeure claims: |
| 1. Impossibility, not mere difficulty or inconvenience |
| 2. Proper notice as per contractual terms |
| 3. Mitigation efforts undertaken by the affected party |
| |
| Application to Current Case: |
| The defendant must demonstrate absolute impossibility of performance, not merely increased cost or delay. |
| The defendant must also show good faith mitigation efforts, such as seeking alternative suppliers. |
| ''', |
| ] |
|
|
|
|
| class AgentState(TypedDict): |
| messages: Annotated[list, add_messages] |
| case_text: str |
| final_analysis: str |
|
|
|
|
| def secrets_ready() -> bool: |
| return bool(os.getenv("OPENAI_API_KEY")) |
|
|
|
|
| @st.cache_resource(show_spinner=False) |
| def create_vectorstore(openai_key: str): |
| docs = [Document(page_content=doc) for doc in LEGAL_KNOWLEDGE_BASE] |
|
|
| text_splitter = RecursiveCharacterTextSplitter( |
| chunk_size=1000, |
| chunk_overlap=200, |
| ) |
| splits = text_splitter.split_documents(docs) |
|
|
| embedding_model = OpenAIEmbeddings( |
| model="text-embedding-3-small", |
| api_key=openai_key, |
| ) |
|
|
| persist_dir = tempfile.mkdtemp(prefix="legal_agent_chroma_") |
|
|
| vectorstore = Chroma.from_documents( |
| documents=splits, |
| embedding=embedding_model, |
| collection_name=f"legal_cases_{uuid.uuid4().hex[:8]}", |
| persist_directory=persist_dir, |
| ) |
|
|
| return vectorstore, len(splits) |
|
|
|
|
| def build_legal_agent(model_name: str, temperature: float = 0.0): |
| openai_key = os.getenv("OPENAI_API_KEY") |
| tavily_key = os.getenv("TAVILY_API_KEY") |
|
|
| llm = ChatOpenAI( |
| model=model_name, |
| temperature=temperature, |
| api_key=openai_key, |
| ) |
|
|
| vectorstore, chunk_count = create_vectorstore(openai_key) |
|
|
| @tool |
| def rag_search_tool(query: str) -> str: |
| """Search legal case documents using RAG for similar precedents and relevant information.""" |
| docs = vectorstore.similarity_search(query, k=3) |
|
|
| if not docs: |
| return "No relevant legal precedents found in the knowledge base." |
|
|
| results = [] |
| for i, doc in enumerate(docs, 1): |
| results.append(f"Precedent {i}:\n{doc.page_content}") |
|
|
| return "\n\n".join(results) |
|
|
| @tool |
| def tavily_web_search_tool(query: str) -> str: |
| """Search the web using Tavily for recent legal cases and legal developments.""" |
| if not tavily_key: |
| return ( |
| "Tavily web search is not configured. " |
| "Add TAVILY_API_KEY in Hugging Face Space secrets to enable live web search." |
| ) |
|
|
| if TavilySearchResults is None: |
| return "Tavily package is not installed. Add tavily-python to requirements.txt." |
|
|
| tavily_tool = TavilySearchResults(max_results=3) |
| results = tavily_tool.invoke({"query": query}) |
|
|
| if not results: |
| return "No web search results found." |
|
|
| formatted_results = [] |
| for i, result in enumerate(results, 1): |
| content = result.get("content", "No content available") |
| url = result.get("url", "No URL") |
| formatted_results.append(f"Result {i}:\n{content}\nSource: {url}") |
|
|
| return "\n\n".join(formatted_results) |
|
|
| @tool |
| def analyze_loopholes_tool(case_summary: str) -> str: |
| """Analyze legal case to identify potential loopholes using LLM reasoning.""" |
| analysis_prompt = f''' |
| You are a senior legal analyst. Analyze the following case and identify potential loopholes. |
| |
| Case Summary: |
| {case_summary} |
| |
| Identify: |
| 1. Procedural loopholes |
| 2. Evidentiary gaps |
| 3. Jurisdictional issues |
| 4. Statute of limitations concerns |
| 5. Constitutional challenges |
| |
| Provide a detailed analysis with reasoning. |
| ''' |
| response = llm.invoke([HumanMessage(content=analysis_prompt)]) |
| return response.content |
|
|
| tools = [rag_search_tool, tavily_web_search_tool, analyze_loopholes_tool] |
|
|
| def agent_node(state: AgentState) -> Dict[str, Any]: |
| system_prompt = SystemMessage( |
| content=f''' |
| You are an expert legal analysis AI agent specializing in identifying loopholes and weaknesses in legal cases. |
| |
| Case to Analyze: |
| {state["case_text"]} |
| |
| Your Task: |
| Conduct a comprehensive legal analysis by: |
| 1. Searching for similar precedent cases using rag_search_tool |
| 2. Finding recent legal developments using tavily_web_search_tool |
| 3. Analyzing loopholes using analyze_loopholes_tool |
| |
| Analysis Framework: |
| - Force majeure defenses and procedural requirements |
| - Notice requirements and timelines |
| - Burden of proof and evidence standards |
| - Contributory negligence considerations |
| - Damages calculation and substantiation |
| |
| After gathering all information, provide a comprehensive final analysis covering: |
| - Case summary |
| - Relevant precedents found |
| - Current legal developments, if available |
| - Procedural loopholes |
| - Evidentiary weaknesses |
| - Risk assessment |
| - Strategic recommendations for plaintiff and defendant |
| |
| Use the tools before concluding. |
| ''' |
| ) |
|
|
| llm_with_tools = llm.bind_tools(tools) |
| response = llm_with_tools.invoke([system_prompt] + state["messages"]) |
| return {"messages": [response]} |
|
|
| def should_continue(state: AgentState) -> str: |
| last_message = state["messages"][-1] |
|
|
| if isinstance(last_message, AIMessage) and getattr(last_message, "tool_calls", None): |
| return "tools" |
|
|
| return "end" |
|
|
| workflow = StateGraph(AgentState) |
| workflow.add_node("agent", agent_node) |
| workflow.add_node("tools", ToolNode(tools)) |
|
|
| workflow.set_entry_point("agent") |
| workflow.add_conditional_edges( |
| "agent", |
| should_continue, |
| { |
| "tools": "tools", |
| "end": END, |
| }, |
| ) |
| workflow.add_edge("tools", "agent") |
|
|
| memory = MemorySaver() |
| graph = workflow.compile(checkpointer=memory) |
|
|
| return graph, chunk_count |
|
|
|
|
| def run_legal_analysis(case_text: str, model_name: str, temperature: float): |
| graph, chunk_count = build_legal_agent(model_name, temperature) |
|
|
| thread_id = f"legal_case_{uuid.uuid4().hex[:8]}" |
| config = {"configurable": {"thread_id": thread_id}} |
|
|
| initial_state = { |
| "messages": [HumanMessage(content="Analyze this legal case for loopholes and weaknesses.")], |
| "case_text": case_text, |
| "final_analysis": "", |
| } |
|
|
| trace = [] |
| final_answer = "" |
|
|
| for step_output in graph.stream(initial_state, config, stream_mode="values"): |
| last_message = step_output["messages"][-1] |
|
|
| if isinstance(last_message, AIMessage): |
| tool_calls = getattr(last_message, "tool_calls", None) |
|
|
| if tool_calls: |
| for call in tool_calls: |
| trace.append( |
| { |
| "type": "tool_call", |
| "name": call.get("name", "Unknown Tool"), |
| "args": call.get("args", {}), |
| } |
| ) |
| elif last_message.content: |
| trace.append( |
| { |
| "type": "agent_message", |
| "content": last_message.content, |
| } |
| ) |
| final_answer = last_message.content |
|
|
| elif isinstance(last_message, ToolMessage): |
| trace.append( |
| { |
| "type": "tool_result", |
| "name": getattr(last_message, "name", "Tool"), |
| "content": last_message.content, |
| } |
| ) |
|
|
| final_state = graph.get_state(config) |
| final_message = final_state.values["messages"][-1] |
| if isinstance(final_message, AIMessage) and final_message.content: |
| final_answer = final_message.content |
|
|
| return { |
| "final_answer": final_answer, |
| "trace": trace, |
| "chunk_count": chunk_count, |
| "thread_id": thread_id, |
| } |
|
|
|
|
| def display_trace(trace: List[Dict[str, Any]]): |
| if not trace: |
| st.info("No trace available yet.") |
| return |
|
|
| for i, item in enumerate(trace, 1): |
| if item["type"] == "tool_call": |
| with st.expander(f"Step {i}: Agent called tool β {item['name']}", expanded=True): |
| st.json(item["args"]) |
|
|
| elif item["type"] == "tool_result": |
| with st.expander(f"Step {i}: Tool result β {item['name']}", expanded=False): |
| st.write(item["content"]) |
|
|
| elif item["type"] == "agent_message": |
| with st.expander(f"Step {i}: Agent response", expanded=False): |
| st.write(item["content"]) |
|
|
|
|
| st.title("βοΈ Legal Agentic AI Project") |
| st.caption("LangGraph ReAct Agent + RAG + Web Search + LLM Reasoning | Hugging Face Spaces Deployment") |
|
|
| with st.sidebar: |
| st.header("Configuration") |
|
|
| model_name = st.selectbox( |
| "OpenAI Model", |
| ["gpt-4o-mini", "gpt-4o", "gpt-4.1-mini"], |
| index=0, |
| ) |
|
|
| temperature = st.slider( |
| "Temperature", |
| min_value=0.0, |
| max_value=1.0, |
| value=0.0, |
| step=0.1, |
| ) |
|
|
| st.divider() |
|
|
| st.subheader("Secrets Status") |
| if os.getenv("OPENAI_API_KEY"): |
| st.success("OPENAI_API_KEY found") |
| else: |
| st.error("OPENAI_API_KEY missing") |
|
|
| if os.getenv("TAVILY_API_KEY"): |
| st.success("TAVILY_API_KEY found") |
| else: |
| st.warning("TAVILY_API_KEY missing - web search will be skipped") |
|
|
| st.divider() |
|
|
| st.markdown( |
| """ |
| **Agent Tools** |
| 1. RAG Search Tool |
| 2. Tavily Web Search Tool |
| 3. Loophole Analysis Tool |
| """ |
| ) |
|
|
|
|
| tab_demo, tab_architecture, tab_code, tab_deployment = st.tabs( |
| ["π Live Demo", "π§ Architecture", "π§© Code Walkthrough", "βοΈ HF Deployment"] |
| ) |
|
|
|
|
| with tab_demo: |
| st.subheader("Run the Legal Agent") |
|
|
| st.markdown( |
| """ |
| This demo takes a legal case, searches an internal legal knowledge base, |
| optionally searches the web using Tavily, analyzes loopholes, and produces a final legal analysis. |
| """ |
| ) |
|
|
| case_text = st.text_area( |
| "Legal Case Input", |
| value=SAMPLE_CASE, |
| height=360, |
| ) |
|
|
| col1, col2 = st.columns([1, 2]) |
|
|
| with col1: |
| run_button = st.button("Run Legal Agent", type="primary", use_container_width=True) |
|
|
| with col2: |
| st.info("For learners: watch the trace below to understand Observe β Reason β Tool Use β Final Answer.") |
|
|
| if run_button: |
| if not secrets_ready(): |
| st.error("OPENAI_API_KEY is missing. Add it in Hugging Face Space β Settings β Repository secrets.") |
| elif not case_text.strip(): |
| st.error("Please enter a legal case.") |
| else: |
| with st.spinner("Agent is analyzing the case..."): |
| try: |
| result = run_legal_analysis(case_text, model_name, temperature) |
| st.session_state["last_result"] = result |
| except Exception as e: |
| st.exception(e) |
|
|
| if "last_result" in st.session_state: |
| result = st.session_state["last_result"] |
|
|
| st.success(f"Analysis complete. RAG initialized with {result['chunk_count']} document chunks.") |
|
|
| st.markdown("## Final Legal Analysis") |
| st.write(result["final_answer"]) |
|
|
| st.markdown("## Agent Execution Trace") |
| display_trace(result["trace"]) |
|
|
|
|
| with tab_architecture: |
| st.subheader("How the Agent Works") |
|
|
| st.markdown( |
| """ |
| The notebook project has been converted into a deployable Streamlit app. |
| |
| The core design is a **ReAct Agent** built using **LangGraph**. |
| |
| **Flow:** |
| |
| ```text |
| User Case |
| β |
| Agent Node |
| β |
| Decide: Need tools? |
| β |
| Tool Node |
| βββ RAG Search Tool |
| βββ Tavily Web Search Tool |
| βββ Loophole Analysis Tool |
| β |
| Agent Node again |
| β |
| Final Legal Analysis |
| ``` |
| """ |
| ) |
|
|
| st.markdown("### What each tool does") |
|
|
| st.table( |
| [ |
| { |
| "Tool": "rag_search_tool", |
| "Purpose": "Searches internal legal knowledge base for precedents.", |
| "Example": "Force majeure notice requirement", |
| }, |
| { |
| "Tool": "tavily_web_search_tool", |
| "Purpose": "Searches current web information using Tavily.", |
| "Example": "Recent force majeure legal developments", |
| }, |
| { |
| "Tool": "analyze_loopholes_tool", |
| "Purpose": "Uses LLM reasoning to identify loopholes and weaknesses.", |
| "Example": "Procedural gaps, evidentiary issues, damages weakness", |
| }, |
| ] |
| ) |
|
|
| st.markdown("### Teaching point") |
| st.info( |
| "This is not just a chatbot. It is an agent because it can reason, decide which tool to use, call tools, observe results, and then continue reasoning." |
| ) |
|
|
|
|
| with tab_code: |
| st.subheader("Code Walkthrough for Learners") |
|
|
| st.markdown("### 1. State") |
| st.code( |
| ''' |
| class AgentState(TypedDict): |
| messages: Annotated[list, add_messages] |
| case_text: str |
| final_analysis: str |
| ''', |
| language="python", |
| ) |
|
|
| st.markdown("### 2. Tools") |
| st.code( |
| ''' |
| tools = [ |
| rag_search_tool, |
| tavily_web_search_tool, |
| analyze_loopholes_tool |
| ] |
| ''', |
| language="python", |
| ) |
|
|
| st.markdown("### 3. Agent Node") |
| st.code( |
| ''' |
| def agent_node(state): |
| system_prompt = SystemMessage(content="Legal analysis instructions...") |
| llm_with_tools = llm.bind_tools(tools) |
| response = llm_with_tools.invoke([system_prompt] + state["messages"]) |
| return {"messages": [response]} |
| ''', |
| language="python", |
| ) |
|
|
| st.markdown("### 4. Conditional Routing") |
| st.code( |
| ''' |
| def should_continue(state): |
| last_message = state["messages"][-1] |
| |
| if last_message.tool_calls: |
| return "tools" |
| |
| return "end" |
| ''', |
| language="python", |
| ) |
|
|
| st.markdown("### 5. LangGraph Workflow") |
| st.code( |
| ''' |
| workflow = StateGraph(AgentState) |
| workflow.add_node("agent", agent_node) |
| workflow.add_node("tools", ToolNode(tools)) |
| |
| workflow.set_entry_point("agent") |
| workflow.add_conditional_edges("agent", should_continue, { |
| "tools": "tools", |
| "end": END |
| }) |
| workflow.add_edge("tools", "agent") |
| |
| graph = workflow.compile(checkpointer=MemorySaver()) |
| ''', |
| language="python", |
| ) |
|
|
|
|
| with tab_deployment: |
| st.subheader("Hugging Face Spaces Deployment") |
|
|
| st.markdown( |
| """ |
| Your Space should have this structure: |
| |
| ```text |
| Hands-On-Agentic-Project/ |
| β |
| βββ app.py |
| βββ Dockerfile |
| βββ requirements.txt |
| βββ README.md |
| β |
| βββ src/ |
| βββ streamlit_app.py |
| ``` |
| |
| **Secrets required:** |
| |
| ```text |
| OPENAI_API_KEY |
| TAVILY_API_KEY |
| ``` |
| |
| `OPENAI_API_KEY` is required. |
| `TAVILY_API_KEY` is optional but recommended for the web-search tool. |
| |
| After uploading this file inside the `src` folder, run: |
| |
| **Factory rebuild** in Hugging Face Spaces. |
| """ |
| ) |
|
|
| st.warning( |
| "This app is for educational demonstration. It is not legal advice and should not be used as a substitute for a qualified legal professional." |
| ) |
|
|