dev commited on
Commit
2fdbf8c
·
1 Parent(s): 655c45a
.gitignore CHANGED
@@ -1,3 +1,18 @@
1
  .env
2
- venv/*
3
- data/*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  .env
2
+ .env.*
3
+ !.env.example
4
+
5
+ venv/
6
+ .venv/
7
+ env/
8
+
9
+ __pycache__/
10
+ *.py[cod]
11
+
12
+ # Local backups / temp
13
+ legal_agentic_system.git.backup/
14
+
15
+ # Optional large local data (kept out of Git by default)
16
+ legal_agentic_system/data/
17
+ legal_agentic_system/faiss_index/
18
+ legal_agentic_system/policy_faiss_index/
README.md ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Citizen Navigator AI Portal
2
+
3
+ A dual-dashboard Flask + LangGraph application for Indian citizen assistance:
4
+ - **⚖️ Legal Resolution Dashboard**: upload a notice/document or describe a case and get official-step guidance.
5
+ - **🎓 Opportunity Finder Dashboard**: submit a detailed profile and get personalized schemes, jobs, innovation grants, missed opportunities, and a practical roadmap.
6
+
7
+ The system uses **hybrid retrieval** for profile intelligence:
8
+ - **Live web search** (official domains via Tavily)
9
+ - **Offline policy RAG** (FAISS over local policy PDFs)
10
+
11
+ ---
12
+
13
+ ## Features
14
+
15
+ - Two independent pipelines (no cross-contamination of legal/profile workflows)
16
+ - Stage-aware legal analysis (`legal_category`, `case_status`)
17
+ - Deep profile filtering (`gender`, `income`, `status`, `skills`, `goal`)
18
+ - Structured profile outputs:
19
+ - `opportunities_json`
20
+ - `missed_opportunities_json`
21
+ - `guiding_roadmap`
22
+ - Dark-mode, dual-tab frontend with async API calls
23
+ - OCR fallback for scanned PDF/image documents
24
+
25
+ ---
26
+
27
+ ## Project Structure
28
+
29
+ ```text
30
+ legal/
31
+ ├── README.md
32
+ ├── .gitignore
33
+ ├── legal_agentic_system/
34
+ │ ├── app.py
35
+ │ ├── main_graph.py
36
+ │ ├── state.py
37
+ │ ├── requirements.txt
38
+ │ ├── templates/
39
+ │ │ └── index.html
40
+ │ ├── agents/
41
+ │ │ ├── data_agent.py
42
+ │ │ ├── legal_pipeline.py
43
+ │ │ ├── profile_pipeline.py
44
+ │ │ ├── policy_rag.py
45
+ │ │ └── search_tool.py
46
+ │ └── ...
47
+ └── venv/ (local)
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Architecture Overview
53
+
54
+ ### 1) Legal Graph
55
+ `data_extraction -> legal_analysis -> END`
56
+
57
+ - `data_extraction`: reads raw text / text file / PDF / image OCR
58
+ - `legal_analysis`: summarizes issue, performs official procedure search, returns legal action plan
59
+
60
+ ### 2) Profile Graph
61
+ `profile_analysis -> END`
62
+
63
+ - `profile_analysis`: combines policy RAG + live search, ranks opportunities, flags missed windows, builds roadmap
64
+
65
+ ---
66
+
67
+ ## Requirements
68
+
69
+ - Python 3.10+
70
+ - Poppler installed (required by `pdf2image`)
71
+ - API keys for Groq and Tavily
72
+
73
+ Install dependencies:
74
+
75
+ ```bash
76
+ cd legal_agentic_system
77
+ python -m venv .venv
78
+ source .venv/bin/activate
79
+ pip install -r requirements.txt
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Environment Variables
85
+
86
+ Create `legal_agentic_system/.env`:
87
+
88
+ ```env
89
+ GROQ_API_KEY=your_groq_key
90
+ TAVILY_API_KEY=your_tavily_key
91
+ HF_TOKEN=your_huggingface_token
92
+ ```
93
+
94
+ Optional policy-RAG corpus:
95
+ - Put policy PDFs in: `legal_agentic_system/policy_docs/`
96
+ - FAISS index is auto-created in: `legal_agentic_system/policy_faiss_index/`
97
+
98
+ ---
99
+
100
+ ## Run Locally
101
+
102
+ ```bash
103
+ cd legal_agentic_system
104
+ python app.py
105
+ ```
106
+
107
+ Open: `http://localhost:5000`
108
+
109
+ ---
110
+
111
+ ## API Endpoints
112
+
113
+ - `POST /api/analyze_legal`
114
+ - Inputs: `raw_text` **or** `file`, plus optional `legal_category`, `case_status`
115
+ - Output: `solution`
116
+
117
+ - `POST /api/analyze_profile`
118
+ - Inputs: profile fields (`user_age`, `user_state`, `user_education`, `user_specialization`, `user_gender`, `annual_income`, `current_status`, `extracurriculars`, `user_goal`)
119
+ - Output: `opportunities`, `missed_opportunities`, `roadmap`, `report`
120
+
121
+ ---
122
+
123
+ ## Why Files May Not Have Appeared on GitHub
124
+
125
+ This repository previously had `legal_agentic_system` tracked as a **nested git link** instead of normal files. That setup was converted so the folder contents are now tracked directly by this repo.
126
+
127
+ After pulling these changes, run:
128
+
129
+ ```bash
130
+ git add -A
131
+ git commit -m "Fix tracking + add README"
132
+ git push
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Notes
138
+
139
+ - Large/generated directories are intentionally gitignored by default:
140
+ - `legal_agentic_system/data/`
141
+ - `legal_agentic_system/faiss_index/`
142
+ - `legal_agentic_system/policy_faiss_index/`
143
+ - If you want these committed, remove those lines from `.gitignore`.
144
+
145
+ ---
146
+
147
+ ## License
148
+
149
+ Use according to your organization/course requirements.
legal_agentic_system DELETED
@@ -1 +0,0 @@
1
- Subproject commit cde3d5423892cec9fcf998c4def1e9a9698b7070
 
 
legal_agentic_system/.gitignore ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment / secrets
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+
6
+ # Python bytecode and caches
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+
11
+ # Virtual environments
12
+ .venv/
13
+ venv/
14
+ env/
15
+ ENV/
16
+
17
+ # Build / packaging artifacts
18
+ build/
19
+ dist/
20
+ *.egg-info/
21
+ .eggs/
22
+ pip-wheel-metadata/
23
+
24
+ # Test / type-check / tooling caches
25
+ .pytest_cache/
26
+ .mypy_cache/
27
+ .ruff_cache/
28
+ .coverage
29
+ .coverage.*
30
+ htmlcov/
31
+
32
+ # IDE / OS files
33
+ .vscode/
34
+ .idea/
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Logs
39
+ *.log
40
+
41
+ # Runtime / temp files
42
+ tmp/
43
+ temp/
44
+ temp_page_*.jpg
45
+
46
+ # Local vector DB / retrieval artifacts
47
+ faiss_index/
48
+ policy_faiss_index/
49
+
50
+ # Local app data (optional large/generated assets)
51
+ data/
legal_agentic_system/agents/__init__.py ADDED
File without changes
legal_agentic_system/agents/data_agent.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ from dotenv import load_dotenv
4
+ from huggingface_hub import InferenceClient
5
+ from pdf2image import convert_from_path
6
+ from pypdf import PdfReader
7
+ from state import LegalGraphState
8
+
9
+ # Load environment variables
10
+ load_dotenv()
11
+
12
+ def encode_image_to_base64(image_path):
13
+ with open(image_path, "rb") as image_file:
14
+ return base64.b64encode(image_file.read()).decode('utf-8')
15
+
16
+ def process_document(state: LegalGraphState):
17
+ # 1. Check if raw text was passed directly
18
+ if state.get("raw_text") and not state.get("file_path"):
19
+ print("---RECEIVED DIRECT TEXT INPUT---")
20
+ return {"raw_text": state["raw_text"], "current_step": "summarization"}
21
+
22
+ file_path = state.get("file_path", "")
23
+ if not file_path or not os.path.exists(file_path):
24
+ raise ValueError("No valid file path or raw text provided.")
25
+
26
+ # 2. Handle Plain Text Files
27
+ if file_path.lower().endswith('.txt'):
28
+ print("---READING PLAIN TEXT FILE---")
29
+ with open(file_path, 'r', encoding='utf-8') as f:
30
+ extracted_text = f.read().strip()
31
+ return {"raw_text": extracted_text, "current_step": "summarization"}
32
+
33
+ # 3. Handle PDFs (Try PyPDF first, fallback to OCR if empty/scanned)
34
+ if file_path.lower().endswith('.pdf'):
35
+ print("---ATTEMPTING FAST TEXT EXTRACTION VIA PYPDF---")
36
+ try:
37
+ reader = PdfReader(file_path)
38
+ extracted_text = ""
39
+ for page in reader.pages:
40
+ text = page.extract_text()
41
+ if text:
42
+ extracted_text += text + "\n"
43
+
44
+ # If PyPDF finds a meaningful amount of text, return it immediately
45
+ if len(extracted_text.strip()) > 50:
46
+ print("---SUCCESSFULLY EXTRACTED TEXT VIA PYPDF---")
47
+ return {"raw_text": extracted_text.strip(), "current_step": "summarization"}
48
+ else:
49
+ print("---PYPDF FOUND NO TEXT (LIKELY SCANNED). FALLING BACK TO OCR---")
50
+ except Exception as e:
51
+ print(f"---PYPDF ERROR: {e}. FALLING BACK TO OCR---")
52
+
53
+ # 4. Fallback: Hugging Face Vision OCR (For Images and Scanned PDFs)
54
+ print("---EXTRACTING TEXT VIA HUGGING FACE VISION API---")
55
+ hf_token = os.getenv("HF_TOKEN")
56
+ if not hf_token:
57
+ raise ValueError("HF_TOKEN is missing. Please check your .env file.")
58
+
59
+ client = InferenceClient(token=hf_token)
60
+ extracted_text = ""
61
+ image_paths = []
62
+
63
+ if file_path.lower().endswith(('.png', '.jpg', '.jpeg')):
64
+ image_paths.append(file_path)
65
+ elif file_path.lower().endswith('.pdf'):
66
+ # Convert the scanned PDF to images
67
+ pages = convert_from_path(file_path, dpi=150)
68
+ for i, page in enumerate(pages):
69
+ temp_path = f"temp_page_{i}.jpg"
70
+ page.save(temp_path, 'JPEG')
71
+ image_paths.append(temp_path)
72
+ else:
73
+ raise ValueError(f"Unsupported file type: {file_path}")
74
+
75
+ # Process images through the Vision Model
76
+ for img_path in image_paths:
77
+ img_b64 = encode_image_to_base64(img_path)
78
+
79
+ messages = [
80
+ {
81
+ "role": "user",
82
+ "content": [
83
+ {"type": "text", "text": "You are a legal OCR system. Extract all readable text from this document image exactly as written. Do not add any commentary."},
84
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}
85
+ ]
86
+ }
87
+ ]
88
+
89
+ try:
90
+ response = client.chat_completion(
91
+ model="meta-llama/Llama-3.2-11B-Vision-Instruct",
92
+ messages=messages,
93
+ max_tokens=1500
94
+ )
95
+ extracted_text += response.choices[0].message.content + "\n\n"
96
+ except Exception as e:
97
+ extracted_text += f"[Error processing image: {e}]\n"
98
+ finally:
99
+ # Clean up generated image pages from PDFs
100
+ if file_path.lower().endswith('.pdf') and os.path.exists(img_path):
101
+ os.remove(img_path)
102
+
103
+ return {"raw_text": extracted_text.strip(), "current_step": "summarization"}
legal_agentic_system/agents/legal_agent.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from langchain_core.prompts import ChatPromptTemplate
4
+ from langchain_groq import ChatGroq
5
+ from state import LegalGraphState
6
+ from rag_setup import constitution_retriever
7
+ from dotenv import load_dotenv
8
+ load_dotenv()
9
+
10
+ groq_api_key = os.getenv("GROQ_API_KEY")
11
+
12
+ llm = ChatGroq(temperature=0.2, model_name="llama-3.1-8b-instant")
13
+
14
+ def suggest_legal_solution(state: LegalGraphState):
15
+ summary = state["summary"]
16
+
17
+ retrieved_docs = constitution_retriever.invoke(summary)
18
+ rag_context = "\n".join([doc.page_content for doc in retrieved_docs])
19
+
20
+ prompt = ChatPromptTemplate.from_messages([
21
+ ("system", """You are an elite Legal Strategist and Expert Counsel in Indian Law.
22
+ Your objective is to analyze the provided case summary alongside the retrieved legal context to formulate a highly accurate, practical, and objective legal solution.
23
+
24
+ The case may involve constitutional rights, civil disputes, criminal offenses, corporate law, or administrative issues. Adapt your analysis to fit the specific facts and context provided.
25
+
26
+ You MUST structure your response strictly using the following headings:
27
+ 1. 📌 Key Legal Issues: (Identify the core disputes, violations, or questions of law)
28
+ 2. ⚖️ Applicable Law & Interpretation: (Explain exactly how the provided legal context applies to the case)
29
+ 3. 🛠️ Suggested Recourse & Strategy: (Provide actionable, step-by-step legal remedies or defense strategies)
30
+ 4. ⚠️ Risk Assessment & Caveats: (Briefly highlight any legal risks, burdens of proof, or exceptions)
31
+
32
+ Maintain a professional, authoritative, and objective legal tone. Do not invent laws; rely strictly on the provided context and established Indian legal principles."""),
33
+ ("human", "Case Summary:\n{summary}\n\nRetrieved Legal Context:\n{context}\n\nGenerate the structured legal analysis:")
34
+ ])
35
+
36
+ chain = prompt | llm
37
+ response = chain.invoke({"summary": summary, "context": rag_context})
38
+
39
+ return {"rag_context": rag_context, "legal_solution": response.content, "current_step": "opportunities"}
legal_agentic_system/agents/legal_pipeline.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from langchain_core.prompts import ChatPromptTemplate
3
+ from langchain_groq import ChatGroq
4
+ from state import CitizenState
5
+ from agents.search_tool import gov_search_tool
6
+
7
+ llm = ChatGroq(
8
+ temperature=0.1,
9
+ model_name="llama-3.1-8b-instant",
10
+ api_key=os.getenv("GROQ_API_KEY"),
11
+ )
12
+
13
+
14
+ def analyze_legal_case(state: CitizenState):
15
+ category = state.get("legal_category", "General")
16
+ status = state.get("case_status", "Unknown Stage")
17
+ raw_text = state.get("raw_text", "")
18
+
19
+ summary_prompt = ChatPromptTemplate.from_messages([
20
+ ("system", "Summarize the core legal issue or dispute from the text in 3 concise bullet points."),
21
+ ("human", "{text}"),
22
+ ])
23
+ summary = (summary_prompt | llm).invoke({"text": (raw_text or "")[:10000]}).content
24
+
25
+ search_query = (
26
+ f"Government of India official procedure for {category} dispute "
27
+ f"at stage: {status}. Case details: {summary}"
28
+ )
29
+ live_data = gov_search_tool.invoke({"query": search_query})
30
+
31
+ solution_prompt = ChatPromptTemplate.from_messages([
32
+ (
33
+ "system",
34
+ """You are a Citizen Legal Advisor. Based on the user's issue and the live government data, provide a structured legal action plan.
35
+ Do not hallucinate laws. Only use the provided live data. Include website links if present in the data.
36
+ Format cleanly with markdown headings:
37
+ - ⚖️ Case Assessment
38
+ - 🛠️ Recommended Official Steps
39
+ - 🏛️ Official Portals / Resources""",
40
+ ),
41
+ (
42
+ "human",
43
+ "Category: {category}\nStage: {status}\nUser Issue: {summary}\n\nLive Gov Data: {live_data}",
44
+ ),
45
+ ])
46
+
47
+ solution = (solution_prompt | llm).invoke(
48
+ {
49
+ "category": category,
50
+ "status": status,
51
+ "summary": summary,
52
+ "live_data": str(live_data),
53
+ }
54
+ ).content
55
+
56
+ return {
57
+ "legal_summary": summary,
58
+ "legal_solution": solution,
59
+ "gov_resources": str(live_data),
60
+ }
legal_agentic_system/agents/policy_rag.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader
3
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
4
+ from langchain_huggingface import HuggingFaceEmbeddings
5
+ from langchain_community.vectorstores import FAISS
6
+
7
+
8
+ def get_policy_retriever(db_path: str = "policy_faiss_index"):
9
+ embeddings = HuggingFaceEmbeddings(
10
+ model_name="sentence-transformers/all-MiniLM-L6-v2",
11
+ model_kwargs={"device": "cpu"},
12
+ )
13
+
14
+ if os.path.exists(db_path):
15
+ return FAISS.load_local(
16
+ db_path,
17
+ embeddings,
18
+ allow_dangerous_deserialization=True,
19
+ ).as_retriever(search_kwargs={"k": 4})
20
+
21
+ docs_dir = "policy_docs"
22
+ if not os.path.exists(docs_dir):
23
+ return None
24
+
25
+ loader = DirectoryLoader(docs_dir, glob="**/*.pdf", loader_cls=PyPDFLoader)
26
+ docs = loader.load()
27
+ if not docs:
28
+ return None
29
+
30
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1500, chunk_overlap=300)
31
+ splits = text_splitter.split_documents(docs)
32
+
33
+ vectorstore = FAISS.from_documents(splits, embeddings)
34
+ vectorstore.save_local(db_path)
35
+
36
+ return vectorstore.as_retriever(search_kwargs={"k": 4})
37
+
38
+
39
+ policy_retriever = get_policy_retriever()
legal_agentic_system/agents/profile_pipeline.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ from langchain_core.prompts import ChatPromptTemplate
5
+ from langchain_groq import ChatGroq
6
+ from state import CitizenState
7
+ from agents.search_tool import gov_search_tool
8
+ from agents.policy_rag import policy_retriever
9
+
10
+ llm = ChatGroq(
11
+ temperature=0.1,
12
+ model_name="llama-3.3-70b-versatile",
13
+ api_key=os.getenv("GROQ_API_KEY"),
14
+ )
15
+
16
+
17
+ def _extract_json_block(text: str):
18
+ try:
19
+ return json.loads(text)
20
+ except Exception:
21
+ pass
22
+
23
+ match = re.search(r"\[.*\]", text, re.DOTALL)
24
+ if match:
25
+ try:
26
+ return json.loads(match.group(0))
27
+ except Exception:
28
+ return []
29
+
30
+ return []
31
+
32
+
33
+ def analyze_profile_opportunities(state: CitizenState):
34
+ age = str(state.get("user_age") or "").strip()
35
+ gender = str(state.get("user_gender") or "").strip()
36
+ income = str(state.get("annual_income") or "").strip()
37
+ user_status = str(state.get("current_status") or "").strip()
38
+ edu = str(state.get("user_education") or "").strip()
39
+ spec = str(state.get("user_specialization") or "").strip()
40
+ loc = str(state.get("user_state") or "").strip()
41
+ extra = str(state.get("extracurriculars") or "").strip()
42
+ goal = str(state.get("user_goal") or "Find best suited opportunities").strip()
43
+
44
+ profile_context = (
45
+ f"Age: {age}, Gender: {gender}, Income: {income}, Status: {user_status}, "
46
+ f"Education: {edu}, Specialization: {spec}, Location: {loc}, Experience: {extra}, Goal: {goal}"
47
+ )
48
+
49
+ rag_query = (
50
+ f"Eligibility and rules for {user_status or 'student'} in {loc or 'India'} "
51
+ f"regarding {goal}. Consider gender {gender} and income {income}."
52
+ )
53
+ rag_context = ""
54
+ if policy_retriever is not None:
55
+ rag_docs = policy_retriever.invoke(rag_query)
56
+ rag_context = "\n".join([doc.page_content for doc in rag_docs])
57
+
58
+ active_query = (
59
+ f"Government of India active schemes, grants, and jobs for {gender} {user_status}, "
60
+ f"{edu} in {spec}, income {income}, located in {loc}"
61
+ )
62
+ active_data = gov_search_tool.invoke({"query": active_query})
63
+
64
+ innovation_query = (
65
+ f"Government of India startup grants, innovation funds, or fellowships "
66
+ f"for students with experience in: {extra}"
67
+ )
68
+ innovation_data = gov_search_tool.invoke({"query": innovation_query})
69
+
70
+ missed_query = (
71
+ f"Recently closed government deadlines, expired schemes, or age limits passed "
72
+ f"for age {age} {spec} graduates in India"
73
+ )
74
+ missed_data = gov_search_tool.invoke({"query": missed_query})
75
+
76
+ opportunities_prompt = ChatPromptTemplate.from_messages([
77
+ (
78
+ "system",
79
+ """You are an elite AI Citizen Advisor.
80
+ Return ONLY valid JSON array with exactly 3 objects. Do not include markdown or prose.
81
+ Each object must contain:
82
+ - name
83
+ - category
84
+ - why_this_suits_you
85
+ - eligibility_fit
86
+ - average_time_to_redeem
87
+ - accessibility_score
88
+ - action_steps (array of short strings)
89
+ - official_link
90
+
91
+ Rules:
92
+ 1) Use only provided RAG context and live government data.
93
+ 2) Filter out schemes/jobs the user is unlikely to qualify for (age, gender, income, status).
94
+ 3) Keep output strict JSON.""",
95
+ ),
96
+ (
97
+ "human",
98
+ "Profile: {profile}\n\nPolicy RAG Context:\n{rag_context}\n\nActive Data: {active}\n\nInnovation Data: {innovation}",
99
+ ),
100
+ ])
101
+
102
+ opportunities_raw = (opportunities_prompt | llm).invoke(
103
+ {
104
+ "profile": profile_context,
105
+ "rag_context": rag_context,
106
+ "active": str(active_data),
107
+ "innovation": str(innovation_data),
108
+ }
109
+ ).content
110
+
111
+ opportunities_json = _extract_json_block(opportunities_raw)
112
+
113
+ missed_prompt = ChatPromptTemplate.from_messages([
114
+ (
115
+ "system",
116
+ """Return ONLY valid JSON array of up to 3 missed opportunities.
117
+ Each object must include:
118
+ - name
119
+ - reason_missed
120
+ - next_cycle_hint
121
+ - expected_next_window
122
+ - official_link""",
123
+ ),
124
+ (
125
+ "human",
126
+ "Profile: {profile}\n\nMissed Data: {missed}",
127
+ ),
128
+ ])
129
+
130
+ missed_raw = (missed_prompt | llm).invoke(
131
+ {
132
+ "profile": profile_context,
133
+ "missed": str(missed_data),
134
+ }
135
+ ).content
136
+ missed_json = _extract_json_block(missed_raw)
137
+
138
+ roadmap_prompt = ChatPromptTemplate.from_messages([
139
+ (
140
+ "system",
141
+ """Create a concise, actionable roadmap in markdown.
142
+ Sections:
143
+ ### 🎯 Core Eligibility Schemes
144
+ ### 🚀 Career & Innovation Grants
145
+ ### ⚠️ Missed Opportunities
146
+ Use empathetic plain language and include practical next actions.""",
147
+ ),
148
+ (
149
+ "human",
150
+ "Profile: {profile}\n\nSelected Opportunities: {opps}\n\nMissed: {missed}",
151
+ ),
152
+ ])
153
+
154
+ roadmap = (roadmap_prompt | llm).invoke(
155
+ {
156
+ "profile": profile_context,
157
+ "opps": json.dumps(opportunities_json, ensure_ascii=False),
158
+ "missed": json.dumps(missed_json, ensure_ascii=False),
159
+ }
160
+ ).content
161
+
162
+ return {
163
+ "active_opportunities": str(active_data),
164
+ "missed_opportunities": str(missed_data),
165
+ "opportunities_json": opportunities_json,
166
+ "missed_opportunities_json": missed_json,
167
+ "guiding_roadmap": roadmap,
168
+ "insight_report": roadmap,
169
+ }
legal_agentic_system/agents/recommendation_agent.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from langchain_community.tools.tavily_search import TavilySearchResults
4
+ from langchain_groq import ChatGroq
5
+ from langchain_core.prompts import ChatPromptTemplate
6
+ from state import LegalGraphState
7
+
8
+ load_dotenv()
9
+ groq_api_key = os.getenv("GROQ_API_KEY")
10
+ tavily_api_key = os.getenv("TAVILY_API_KEY")
11
+
12
+ llm = ChatGroq(temperature=0.3, model_name="llama-3.1-8b-instant", api_key=groq_api_key)
13
+ search_tool = TavilySearchResults(max_results=4, tavily_api_key=tavily_api_key)
14
+
15
+ def suggest_opportunities(state: LegalGraphState):
16
+ age = str(state.get("user_age") or "Unknown").strip()
17
+ edu = str(state.get("user_education") or "Unknown").strip()
18
+ spec = str(state.get("user_specialization") or "Unknown").strip()
19
+ loc = str(state.get("user_state") or "India").strip()
20
+
21
+ if edu == "Unknown" and spec == "Unknown":
22
+ return {
23
+ "recommended_policies": "",
24
+ "recommended_exams_jobs": "",
25
+ "current_step": "report_generation",
26
+ }
27
+
28
+ policy_query = f"Government of India schemes, scholarships, or policies for {edu} students in {spec} from {loc}"
29
+ policy_results = search_tool.invoke({"query": policy_query})
30
+
31
+ career_query = f"Upcoming government exams and tech jobs in India for {spec} graduates age {age}"
32
+ career_results = search_tool.invoke({"query": career_query})
33
+
34
+ prompt = ChatPromptTemplate.from_messages([
35
+ (
36
+ "system",
37
+ """You are a helpful Career and Policy Advisor for Indian citizens.
38
+ Based on the user's profile and the live search results, format two clean, distinct sections:
39
+ 1. **Beneficial Government Policies & Schemes** (Include actionable steps and website links if available).
40
+ 2. **Relevant Government Exams & Job Opportunities** (Tailored to their education and field).
41
+
42
+ Keep the tone encouraging, accessible, and completely free of AI or technical jargon.""",
43
+ ),
44
+ (
45
+ "human",
46
+ "User Profile: Age {age}, Education: {edu} in {spec}, Location: {loc}\n\nPolicy Data: {policy_data}\n\nCareer Data: {career_data}",
47
+ ),
48
+ ])
49
+
50
+ chain = prompt | llm
51
+ response = chain.invoke(
52
+ {
53
+ "age": age,
54
+ "edu": edu,
55
+ "spec": spec,
56
+ "loc": loc,
57
+ "policy_data": str(policy_results),
58
+ "career_data": str(career_results),
59
+ }
60
+ )
61
+
62
+ return {
63
+ "recommended_policies": response.content,
64
+ "recommended_exams_jobs": response.content,
65
+ "current_step": "report_generation",
66
+ }
legal_agentic_system/agents/report_agent.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from langchain_core.prompts import ChatPromptTemplate
4
+ from langchain_groq import ChatGroq
5
+ from state import LegalGraphState
6
+
7
+ load_dotenv()
8
+ groq_api_key = os.getenv("GROQ_API_KEY")
9
+ llm = ChatGroq(temperature=0.1, model_name="llama-3.1-8b-instant", api_key=groq_api_key)
10
+
11
+ def compile_final_report(state: LegalGraphState):
12
+ summary = state.get("summary", "")
13
+ solution = state.get("legal_solution", "")
14
+ policies = state.get("recommended_policies", "")
15
+ exams_jobs = state.get("recommended_exams_jobs", "")
16
+
17
+ prompt = ChatPromptTemplate.from_messages([
18
+ (
19
+ "system",
20
+ """You are an expert public servant and citizen advocate. Your job is to compile the provided legal analysis and policy recommendations into one comprehensive, easy-to-read Final Report.
21
+
22
+ Rules:
23
+ 1. STRIP ALL JARGON: Do not use terms like "LLM", "RAG", "Agent", "Context", or "Prompt".
24
+ 2. Use clear headings, bullet points, and bold text for readability.
25
+ 3. Structure the report as follows:
26
+ - 📝 Case Overview (Simplify the legal summary)
27
+ - ⚖️ Actionable Legal Guidance (Simplify the legal solution)
28
+ - 🎓 Citizen Opportunities (Display the policies, exams, and jobs)
29
+ - 🌐 Next Steps & Support Links
30
+ 4. Speak directly to the user in a helpful, empathetic, and professional tone.""",
31
+ ),
32
+ (
33
+ "human",
34
+ "Legal Summary: {summary}\n\nLegal Solution: {solution}\n\nPolicies: {policies}\n\nExams and Jobs: {exams_jobs}",
35
+ ),
36
+ ])
37
+
38
+ chain = prompt | llm
39
+ response = chain.invoke(
40
+ {
41
+ "summary": summary,
42
+ "solution": solution,
43
+ "policies": policies,
44
+ "exams_jobs": exams_jobs,
45
+ }
46
+ )
47
+
48
+ return {"final_report": response.content, "current_step": "end"}
legal_agentic_system/agents/resource_agent.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from langchain_community.tools.tavily_search import TavilySearchResults
4
+ from langchain_groq import ChatGroq
5
+ from langchain_core.prompts import ChatPromptTemplate
6
+ from state import LegalGraphState
7
+ from dotenv import load_dotenv
8
+ load_dotenv()
9
+
10
+ groq_api_key = os.getenv("GROQ_API_KEY")
11
+
12
+ llm = ChatGroq(temperature=0, model_name="llama-3.3-70b-versatile")
13
+ search_tool = TavilySearchResults(max_results=3)
14
+
15
+ def find_gov_resources(state: LegalGraphState):
16
+ solution = state["legal_solution"]
17
+
18
+ search_query = f"official government of India portal or scheme for: {solution[:100]}"
19
+ search_results = search_tool.invoke({"query": search_query})
20
+
21
+ prompt = ChatPromptTemplate.from_messages([
22
+ ("system", "You are an administrative assistant. Format the following web search results into a clean list of actionable government steps, resources, and official website links. Ensure links are included if available."),
23
+ ("human", "Search Results: {results}")
24
+ ])
25
+
26
+ chain = prompt | llm
27
+ response = chain.invoke({"results": str(search_results)})
28
+
29
+ return {"government_resources": [response.content], "current_step": "end"}
legal_agentic_system/agents/search_tool.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from langchain_community.tools.tavily_search import TavilySearchResults
4
+
5
+ load_dotenv()
6
+ tavily_api_key = os.getenv("TAVILY_API_KEY")
7
+
8
+ gov_search_tool = TavilySearchResults(
9
+ max_results=4,
10
+ search_depth="advanced",
11
+ include_domains=["gov.in", "nic.in", "india.gov.in"],
12
+ tavily_api_key=tavily_api_key,
13
+ )
legal_agentic_system/agents/summary_agent.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from langchain_core.prompts import ChatPromptTemplate
4
+ from langchain_groq import ChatGroq
5
+ from state import LegalGraphState
6
+ from dotenv import load_dotenv
7
+ load_dotenv()
8
+
9
+ groq_api_key = os.getenv("GROQ_API_KEY")
10
+
11
+ llm = ChatGroq(temperature=0, model_name="llama-3.3-70b-versatile")
12
+
13
+ def summarize_document(state: LegalGraphState):
14
+ raw_text = state["raw_text"]
15
+
16
+ prompt = ChatPromptTemplate.from_messages([
17
+ ("system", "You are an expert legal clerk. Extract the core legal dispute, parties involved, and key facts from the following text. Keep it objective and concise."),
18
+ ("human", "Legal Document: \n\n{text}")
19
+ ])
20
+
21
+ chain = prompt | llm
22
+ # Truncate text slightly to ensure it fits in the context window
23
+ response = chain.invoke({"text": raw_text[:20000]})
24
+
25
+ return {"summary": response.content, "current_step": "legal_analysis"}
legal_agentic_system/app.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ from flask import Flask, request, render_template, jsonify
4
+ from werkzeug.utils import secure_filename
5
+ from dotenv import load_dotenv
6
+ from main_graph import legal_graph, profile_graph
7
+
8
+ load_dotenv()
9
+
10
+ app = Flask(__name__)
11
+ app.secret_key = os.urandom(24)
12
+
13
+ ALLOWED_EXTENSIONS = {'pdf', 'png', 'jpg', 'jpeg', 'txt'}
14
+
15
+ def allowed_file(filename):
16
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
17
+
18
+ @app.route('/')
19
+ def index():
20
+ return render_template('index.html')
21
+
22
+
23
+ @app.route('/api/analyze_legal', methods=['POST'])
24
+ def analyze_legal():
25
+ raw_text = request.form.get('raw_text', '').strip()
26
+ inputs = {}
27
+ temp_path = None
28
+
29
+ inputs["legal_category"] = request.form.get('legal_category', 'General')
30
+ inputs["case_status"] = request.form.get('case_status', 'Need Information')
31
+
32
+ if raw_text:
33
+ inputs["raw_text"] = raw_text
34
+ inputs["file_path"] = None
35
+ elif 'file' in request.files and request.files['file'].filename != '':
36
+ file = request.files['file']
37
+ if not allowed_file(file.filename):
38
+ return jsonify({"success": False, "error": "Invalid file type. Please upload a PDF, TXT, JPG, or PNG."}), 400
39
+
40
+ filename = secure_filename(file.filename)
41
+ temp_path = os.path.join(tempfile.gettempdir(), filename)
42
+ file.save(temp_path)
43
+ inputs["file_path"] = temp_path
44
+ inputs["raw_text"] = None
45
+ else:
46
+ return jsonify({"success": False, "error": "No file or text provided."}), 400
47
+
48
+ try:
49
+ final_state = legal_graph.invoke(inputs)
50
+ return jsonify({
51
+ "success": True,
52
+ "solution": final_state.get("legal_solution", "Analysis failed."),
53
+ })
54
+ except Exception as e:
55
+ return jsonify({"success": False, "error": str(e)}), 500
56
+ finally:
57
+ if temp_path and os.path.exists(temp_path):
58
+ os.remove(temp_path)
59
+
60
+
61
+ @app.route('/api/analyze_profile', methods=['POST'])
62
+ def analyze_profile():
63
+ inputs = {
64
+ "user_age": request.form.get('user_age', ''),
65
+ "user_state": request.form.get('user_state', ''),
66
+ "user_education": request.form.get('user_education', ''),
67
+ "user_specialization": request.form.get('user_specialization', ''),
68
+ "user_gender": request.form.get('user_gender', ''),
69
+ "annual_income": request.form.get('annual_income', ''),
70
+ "current_status": request.form.get('current_status', ''),
71
+ "extracurriculars": request.form.get('extracurriculars', ''),
72
+ "user_goal": request.form.get('user_goal', ''),
73
+ }
74
+
75
+ try:
76
+ final_state = profile_graph.invoke(inputs)
77
+ return jsonify({
78
+ "success": True,
79
+ "opportunities": final_state.get("opportunities_json", []),
80
+ "missed_opportunities": final_state.get("missed_opportunities_json", []),
81
+ "roadmap": final_state.get("guiding_roadmap", ""),
82
+ "report": final_state.get("insight_report", "Report generation failed."),
83
+ })
84
+ except Exception as e:
85
+ return jsonify({"success": False, "error": str(e)}), 500
86
+
87
+ if __name__ == '__main__':
88
+ app.run(debug=True, host='0.0.0.0', port=5000)
legal_agentic_system/main_graph.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, END
2
+ from state import CitizenState
3
+ from agents.data_agent import process_document
4
+ from agents.legal_pipeline import analyze_legal_case
5
+ from agents.profile_pipeline import analyze_profile_opportunities
6
+
7
+ # --- GRAPH 1: Legal Dashboard ---
8
+ def build_legal_graph():
9
+ workflow = StateGraph(CitizenState)
10
+ workflow.add_node("data_extraction", process_document)
11
+ workflow.add_node("legal_analysis", analyze_legal_case)
12
+
13
+ workflow.set_entry_point("data_extraction")
14
+ workflow.add_edge("data_extraction", "legal_analysis")
15
+ workflow.add_edge("legal_analysis", END)
16
+
17
+ return workflow.compile()
18
+
19
+
20
+ # --- GRAPH 2: Profile Dashboard ---
21
+ def build_profile_graph():
22
+ workflow = StateGraph(CitizenState)
23
+ workflow.add_node("profile_analysis", analyze_profile_opportunities)
24
+
25
+ workflow.set_entry_point("profile_analysis")
26
+ workflow.add_edge("profile_analysis", END)
27
+
28
+ return workflow.compile()
29
+
30
+ legal_graph = build_legal_graph()
31
+ profile_graph = build_profile_graph()
legal_agentic_system/rag_setup.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from langchain_community.document_loaders import PyPDFLoader
3
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
4
+ from langchain_huggingface import HuggingFaceEmbeddings
5
+ from langchain_community.vectorstores import FAISS
6
+
7
+ def initialize_vector_store(pdf_path="data/indian_constitution.pdf", db_path="faiss_index"):
8
+ embeddings = HuggingFaceEmbeddings(
9
+ model_name="sentence-transformers/all-MiniLM-L6-v2",
10
+ model_kwargs={'device': 'cpu'}
11
+ )
12
+
13
+ if os.path.exists(db_path):
14
+ return FAISS.load_local(db_path, embeddings, allow_dangerous_deserialization=True)
15
+
16
+ if not os.path.exists(pdf_path):
17
+ raise FileNotFoundError(f"Please place the constitution PDF at {pdf_path}")
18
+
19
+ loader = PyPDFLoader(pdf_path)
20
+ docs = loader.load()
21
+
22
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
23
+ splits = text_splitter.split_documents(docs)
24
+
25
+ vectorstore = FAISS.from_documents(splits, embeddings)
26
+ vectorstore.save_local(db_path)
27
+ return vectorstore
28
+
29
+ # Export a retriever instance
30
+ vectorstore = initialize_vector_store()
31
+ constitution_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
legal_agentic_system/requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask
2
+ Werkzeug
3
+ huggingface_hub
4
+ pdf2image
5
+ Pillow
6
+ langchain
7
+ langchain-groq
8
+ langchain-community
9
+ langchain-huggingface
10
+ langchain-text-splitters
11
+ langgraph
12
+ tavily-python
13
+ faiss-cpu
14
+ python-dotenv
15
+ pypdf
16
+ sentence-transformer
legal_agentic_system/state.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TypedDict, Optional
2
+
3
+
4
+ class CitizenState(TypedDict):
5
+ # --- Legal Dashboard Inputs ---
6
+ file_path: Optional[str]
7
+ raw_text: Optional[str]
8
+ legal_category: Optional[str]
9
+ case_status: Optional[str]
10
+
11
+ # --- Profile Dashboard Inputs ---
12
+ user_age: Optional[str]
13
+ user_state: Optional[str]
14
+ user_education: Optional[str]
15
+ user_specialization: Optional[str]
16
+ user_gender: Optional[str]
17
+ annual_income: Optional[str]
18
+ current_status: Optional[str]
19
+ extracurriculars: Optional[str]
20
+ user_goal: Optional[str]
21
+
22
+ # --- Legal Outputs ---
23
+ legal_summary: str
24
+ legal_solution: str
25
+ gov_resources: str
26
+
27
+ # --- Profile Outputs ---
28
+ active_opportunities: str
29
+ missed_opportunities: str
30
+ opportunities_json: list[dict]
31
+ missed_opportunities_json: list[dict]
32
+ guiding_roadmap: str
33
+ insight_report: str
34
+
35
+
36
+ # Backward compatibility for existing modules still importing LegalGraphState
37
+ LegalGraphState = CitizenState
legal_agentic_system/templates/index.html ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Citizen Navigator | Dual Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10
+ <script>
11
+ tailwind.config = { darkMode: 'class' }
12
+ </script>
13
+ <style>
14
+ .markdown-body h3 { font-size: 1.25rem; font-weight: 700; margin-top: 1.5em; margin-bottom: 0.75em; border-bottom: 1px solid #e2e8f0; padding-bottom: 0.3em; }
15
+ .markdown-body p { margin-bottom: 1em; line-height: 1.6; }
16
+ .markdown-body ul { margin-left: 1.5em; margin-bottom: 1em; list-style-type: disc; }
17
+ .markdown-body a { color: #2563eb; text-decoration: underline; font-weight: 500; }
18
+ .dark .markdown-body h3 { border-color: #334155; color: white; }
19
+ .dark .markdown-body p, .dark .markdown-body li { color: #cbd5e1; }
20
+ .dark .markdown-body a { color: #60a5fa; }
21
+ </style>
22
+ </head>
23
+ <body class="bg-slate-50 dark:bg-slate-900 min-h-screen text-slate-800 dark:text-slate-200 transition-colors">
24
+
25
+ <nav class="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-6 py-4 sticky top-0 z-10">
26
+ <div class="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
27
+ <div class="flex items-center gap-3">
28
+ <div class="bg-blue-600 text-white p-2 rounded-lg"><i class="fa-solid fa-building-columns"></i></div>
29
+ <h1 class="text-xl font-bold">Citizen Navigator</h1>
30
+ </div>
31
+
32
+ <div class="flex bg-slate-100 dark:bg-slate-700 p-1 rounded-lg">
33
+ <button onclick="switchTab('legal')" id="btn-legal" class="px-4 py-2 text-sm font-medium rounded-md bg-white dark:bg-slate-800 shadow-sm transition-all">⚖️ Legal Resolution</button>
34
+ <button onclick="switchTab('profile')" id="btn-profile" class="px-4 py-2 text-sm font-medium rounded-md text-slate-500 dark:text-slate-400 hover:text-slate-700 transition-all">🎓 Opportunity Finder</button>
35
+ </div>
36
+
37
+ <button onclick="document.documentElement.classList.toggle('dark')" class="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700">
38
+ <i class="fa-solid fa-circle-half-stroke"></i>
39
+ </button>
40
+ </div>
41
+ </nav>
42
+
43
+ <main class="max-w-5xl mx-auto px-6 py-8">
44
+
45
+ <div id="view-legal" class="block space-y-8">
46
+ <div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border border-slate-200 dark:border-slate-700">
47
+ <h2 class="text-lg font-bold mb-4">Analyze Legal Issue</h2>
48
+ <form id="legal-form" class="space-y-4">
49
+ <div class="grid grid-cols-2 gap-4 mb-4">
50
+ <div>
51
+ <label class="block text-sm font-medium mb-1">Domain</label>
52
+ <select name="legal_category" class="w-full border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
53
+ <option value="Cyber Crime">Cyber Crime</option>
54
+ <option value="Tax & Finance">Tax & Finance</option>
55
+ <option value="Civil Dispute">Civil Dispute</option>
56
+ <option value="Corporate/Startup">Corporate/Startup</option>
57
+ </select>
58
+ </div>
59
+ <div>
60
+ <label class="block text-sm font-medium mb-1">Current Stage</label>
61
+ <select name="case_status" class="w-full border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
62
+ <option value="Need Information">Just need information</option>
63
+ <option value="Received Notice">Received a Government/Legal Notice</option>
64
+ <option value="Active Case">Active Court Case</option>
65
+ </select>
66
+ </div>
67
+ </div>
68
+ <div>
69
+ <label class="block text-sm font-medium mb-1">Upload Notice/Document</label>
70
+ <input type="file" name="file" accept=".pdf,.png,.jpg,.txt" class="w-full text-sm border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
71
+ </div>
72
+ <div>
73
+ <label class="block text-sm font-medium mb-1">Or Describe Situation</label>
74
+ <textarea name="raw_text" rows="3" class="w-full border p-3 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600" placeholder="E.g., I received a tax notice..."></textarea>
75
+ </div>
76
+ <button type="submit" id="btn-submit-legal" class="w-full bg-blue-600 text-white py-3 rounded-xl font-medium hover:bg-blue-700">Find Legal Recourse</button>
77
+ </form>
78
+ </div>
79
+
80
+ <div id="legal-loading" class="hidden text-center py-10 animate-pulse">Querying official government databases...</div>
81
+ <div id="legal-results" class="hidden bg-white dark:bg-slate-800 rounded-2xl p-8 shadow-sm border border-slate-200 dark:border-slate-700">
82
+ <div id="legal-output" class="markdown-body"></div>
83
+ </div>
84
+ </div>
85
+
86
+ <div id="view-profile" class="hidden space-y-8">
87
+ <div class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border border-slate-200 dark:border-slate-700">
88
+ <h2 class="text-lg font-bold mb-4">Discover Schemes & Missed Opportunities</h2>
89
+ <form id="profile-form" class="space-y-4">
90
+ <div class="grid grid-cols-2 gap-4">
91
+ <div>
92
+ <label class="block text-sm font-medium mb-1">Age</label>
93
+ <input type="number" name="user_age" placeholder="20" class="w-full border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
94
+ </div>
95
+ <div>
96
+ <label class="block text-sm font-medium mb-1">State</label>
97
+ <input type="text" name="user_state" placeholder="e.g., Uttar Pradesh" class="w-full border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
98
+ </div>
99
+ </div>
100
+ <div>
101
+ <label class="block text-sm font-medium mb-1">Education Level</label>
102
+ <input type="text" name="user_education" placeholder="e.g., 3rd Year B.Tech" class="w-full border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
103
+ </div>
104
+ <div>
105
+ <label class="block text-sm font-medium mb-1">Specialization</label>
106
+ <input type="text" name="user_specialization" placeholder="e.g., CS (AIML & IoT)" class="w-full border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
107
+ </div>
108
+ <div class="grid grid-cols-2 gap-4 mt-4">
109
+ <div>
110
+ <label class="block text-sm font-medium mb-1">Gender</label>
111
+ <select name="user_gender" class="w-full border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
112
+ <option value="Male">Male</option>
113
+ <option value="Female">Female</option>
114
+ <option value="Other">Other</option>
115
+ </select>
116
+ </div>
117
+ <div>
118
+ <label class="block text-sm font-medium mb-1">Annual Family Income</label>
119
+ <select name="annual_income" class="w-full border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
120
+ <option value="Below 2.5 Lakhs">Below 2.5 Lakhs</option>
121
+ <option value="2.5 to 8 Lakhs">2.5 to 8 Lakhs</option>
122
+ <option value="Above 8 Lakhs">Above 8 Lakhs</option>
123
+ </select>
124
+ </div>
125
+ </div>
126
+ <div class="mt-4">
127
+ <label class="block text-sm font-medium mb-1">Current Status</label>
128
+ <select name="current_status" class="w-full border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
129
+ <option value="Student">Student</option>
130
+ <option value="Unemployed">Unemployed</option>
131
+ <option value="Working Professional">Working Professional</option>
132
+ <option value="Entrepreneur">Entrepreneur</option>
133
+ </select>
134
+ </div>
135
+ <div class="mt-4">
136
+ <label class="block text-sm font-medium mb-1">Primary Goal (Optional)</label>
137
+ <input type="text" name="user_goal" placeholder="e.g., Start an AI company" class="w-full border p-2 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600">
138
+ </div>
139
+ <div class="mt-4">
140
+ <label class="block text-sm font-medium mb-1">Projects, Leadership, or Skills (Optional)</label>
141
+ <textarea name="extracurriculars" rows="2" class="w-full border p-3 rounded-lg bg-slate-50 dark:bg-slate-700 dark:border-slate-600" placeholder="E.g., Built machine learning apps, led a university tech club..."></textarea>
142
+ </div>
143
+ <button type="submit" id="btn-submit-profile" class="w-full bg-teal-600 text-white py-3 rounded-xl font-medium hover:bg-teal-700">Generate Insight Report</button>
144
+ </form>
145
+ </div>
146
+
147
+ <div id="profile-loading" class="hidden text-center py-10 animate-pulse">Analyzing active and expired schemes...</div>
148
+ <div id="profile-results" class="hidden bg-white dark:bg-slate-800 rounded-2xl p-8 shadow-sm border border-slate-200 dark:border-slate-700">
149
+ <div id="profile-opportunities" class="space-y-3 mb-6"></div>
150
+ <div id="profile-missed" class="space-y-3 mb-6"></div>
151
+ <div id="profile-output" class="markdown-body"></div>
152
+ </div>
153
+ </div>
154
+
155
+ </main>
156
+
157
+ <script>
158
+ function switchTab(tab) {
159
+ const isLegal = tab === 'legal';
160
+
161
+ document.getElementById('view-legal').classList.toggle('hidden', !isLegal);
162
+ document.getElementById('view-legal').classList.toggle('block', isLegal);
163
+
164
+ document.getElementById('view-profile').classList.toggle('hidden', isLegal);
165
+ document.getElementById('view-profile').classList.toggle('block', !isLegal);
166
+
167
+ const btnClassActive = "px-4 py-2 text-sm font-medium rounded-md bg-white dark:bg-slate-800 shadow-sm transition-all text-slate-800 dark:text-white";
168
+ const btnClassInactive = "px-4 py-2 text-sm font-medium rounded-md text-slate-500 dark:text-slate-400 hover:text-slate-700 transition-all bg-transparent shadow-none";
169
+
170
+ document.getElementById('btn-legal').className = isLegal ? btnClassActive : btnClassInactive;
171
+ document.getElementById('btn-profile').className = !isLegal ? btnClassActive : btnClassInactive;
172
+ }
173
+
174
+ async function handleForm(e, endpoint, btnId, loadingId, resultsId, outputId) {
175
+ e.preventDefault();
176
+ const btn = document.getElementById(btnId);
177
+ const loading = document.getElementById(loadingId);
178
+ const results = document.getElementById(resultsId);
179
+
180
+ btn.disabled = true;
181
+ btn.innerText = 'Processing...';
182
+ results.classList.add('hidden');
183
+ loading.classList.remove('hidden');
184
+
185
+ try {
186
+ const response = await fetch(endpoint, { method: 'POST', body: new FormData(e.target) });
187
+ const data = await response.json();
188
+
189
+ if (data.success) {
190
+ if (endpoint === '/api/analyze_profile') {
191
+ const oppsBox = document.getElementById('profile-opportunities');
192
+ const missedBox = document.getElementById('profile-missed');
193
+ const opps = Array.isArray(data.opportunities) ? data.opportunities : [];
194
+ const missed = Array.isArray(data.missed_opportunities) ? data.missed_opportunities : [];
195
+
196
+ oppsBox.innerHTML = opps.length
197
+ ? '<h3 class="text-lg font-semibold mb-2">🎯 Recommended Opportunities</h3>' + opps.map(item => `\n <div class="border border-slate-200 dark:border-slate-700 rounded-lg p-4">\n <div class="font-semibold">${item.name || 'Opportunity'}</div>\n <div class="text-sm text-slate-500 dark:text-slate-400">${item.category || ''}</div>\n <div class="text-sm mt-2">${item.why_this_suits_you || ''}</div>\n <div class="text-xs mt-2 text-slate-500 dark:text-slate-400">Eligibility: ${item.eligibility_fit || 'N/A'} | Time: ${item.average_time_to_redeem || 'N/A'} | Accessibility: ${item.accessibility_score || 'N/A'}</div>\n </div>`).join('')
198
+ : '';
199
+
200
+ missedBox.innerHTML = missed.length
201
+ ? '<h3 class="text-lg font-semibold mb-2">⚠️ Missed Opportunities</h3>' + missed.map(item => `\n <div class="border border-amber-200 dark:border-amber-700 rounded-lg p-4">\n <div class="font-semibold">${item.name || 'Opportunity'}</div>\n <div class="text-sm mt-1">${item.reason_missed || ''}</div>\n <div class="text-sm mt-1 text-slate-500 dark:text-slate-400">Next: ${item.next_cycle_hint || ''} (${item.expected_next_window || 'TBD'})</div>\n </div>`).join('')
202
+ : '';
203
+
204
+ const roadmapText = data.roadmap || data.report || '';
205
+ document.getElementById(outputId).innerHTML = marked.parse(roadmapText);
206
+ } else {
207
+ const outputText = data.solution || data.report;
208
+ document.getElementById(outputId).innerHTML = marked.parse(outputText);
209
+ }
210
+ results.classList.remove('hidden');
211
+ } else {
212
+ alert("Error: " + data.error);
213
+ }
214
+ } catch (err) {
215
+ alert("Server connection failed.");
216
+ } finally {
217
+ btn.disabled = false;
218
+ btn.innerText = btnId.includes('legal') ? 'Find Legal Recourse' : 'Generate Insight Report';
219
+ loading.classList.add('hidden');
220
+ }
221
+ }
222
+
223
+ document.getElementById('legal-form').addEventListener('submit', (e) => handleForm(e, '/api/analyze_legal', 'btn-submit-legal', 'legal-loading', 'legal-results', 'legal-output'));
224
+ document.getElementById('profile-form').addEventListener('submit', (e) => handleForm(e, '/api/analyze_profile', 'btn-submit-profile', 'profile-loading', 'profile-results', 'profile-output'));
225
+ </script>
226
+ </body>
227
+ </html>