dev commited on
Commit ·
2fdbf8c
1
Parent(s): 655c45a
v1
Browse files- .gitignore +17 -2
- README.md +149 -0
- legal_agentic_system +0 -1
- legal_agentic_system/.gitignore +51 -0
- legal_agentic_system/agents/__init__.py +0 -0
- legal_agentic_system/agents/data_agent.py +103 -0
- legal_agentic_system/agents/legal_agent.py +39 -0
- legal_agentic_system/agents/legal_pipeline.py +60 -0
- legal_agentic_system/agents/policy_rag.py +39 -0
- legal_agentic_system/agents/profile_pipeline.py +169 -0
- legal_agentic_system/agents/recommendation_agent.py +66 -0
- legal_agentic_system/agents/report_agent.py +48 -0
- legal_agentic_system/agents/resource_agent.py +29 -0
- legal_agentic_system/agents/search_tool.py +13 -0
- legal_agentic_system/agents/summary_agent.py +25 -0
- legal_agentic_system/app.py +88 -0
- legal_agentic_system/main_graph.py +31 -0
- legal_agentic_system/rag_setup.py +31 -0
- legal_agentic_system/requirements.txt +16 -0
- legal_agentic_system/state.py +37 -0
- legal_agentic_system/templates/index.html +227 -0
.gitignore
CHANGED
|
@@ -1,3 +1,18 @@
|
|
| 1 |
.env
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|