Spaces:
Sleeping
Sleeping
Kushal Shah Claude Sonnet 4.6 commited on
Commit ·
bd510a2
0
Parent(s):
Initial commit: AI Legal Compliance Auditor
Browse filesGradio-based compliance auditing app with agentic LangChain pipeline,
configured for Hugging Face Spaces deployment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- .gitignore +24 -0
- AGENTIC_SYSTEM_README.md +178 -0
- README.md +106 -0
- agentic_auditor.py +370 -0
- agentic_tools.py +270 -0
- auditor.py +211 -0
- cache_manager.py +131 -0
- database_manager.py +218 -0
- doc_utils.py +84 -0
- document_processor.py +214 -0
- embeddings.py +169 -0
- gradio_app.py +253 -0
- initialize_database.py +82 -0
- list_gemini_models.py +62 -0
- main.py +214 -0
- requirements.txt +23 -0
- retrieval.py +225 -0
- vector_db.py +282 -0
.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment variables
|
| 2 |
+
.env
|
| 3 |
+
|
| 4 |
+
# Vector database
|
| 5 |
+
chroma_db/
|
| 6 |
+
reference_docs/
|
| 7 |
+
*.db
|
| 8 |
+
*.sqlite
|
| 9 |
+
|
| 10 |
+
# Personal notes
|
| 11 |
+
INTERVIEW_SCRIPT.md
|
| 12 |
+
|
| 13 |
+
# Python
|
| 14 |
+
__pycache__/
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Virtual environments
|
| 18 |
+
venv/
|
| 19 |
+
env/
|
| 20 |
+
ENV/
|
| 21 |
+
|
| 22 |
+
# Cache
|
| 23 |
+
.cache/
|
| 24 |
+
*.cache
|
AGENTIC_SYSTEM_README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agentic AI System - Implementation Guide
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This project has been upgraded to include **true Agentic AI capabilities** with multi-step reasoning, tool calling, and autonomous decision-making.
|
| 6 |
+
|
| 7 |
+
## What's New: Agentic Features
|
| 8 |
+
|
| 9 |
+
### 🤖 Multi-Agent Architecture
|
| 10 |
+
|
| 11 |
+
1. **Planning Agent**: Breaks down the audit into steps
|
| 12 |
+
2. **Execution Agent**: Performs the audit using tools
|
| 13 |
+
3. **Refinement Agent**: Reviews and improves results
|
| 14 |
+
|
| 15 |
+
### 🛠️ Available Tools
|
| 16 |
+
|
| 17 |
+
The agent can autonomously call these tools:
|
| 18 |
+
|
| 19 |
+
1. **`search_regulations`**: Search through regulation documents for specific requirements
|
| 20 |
+
2. **`analyze_document_structure`**: Analyze document tone, terminology, and formatting
|
| 21 |
+
3. **`check_missing_fields`**: Check for required fields (Date, CIN, Signature, etc.)
|
| 22 |
+
4. **`compare_with_regulation`**: Compare document clauses against regulations
|
| 23 |
+
5. **`generate_style_adapted_clause`**: Generate clauses that match document style
|
| 24 |
+
6. **`calculate_compliance_score`**: Calculate compliance risk score
|
| 25 |
+
|
| 26 |
+
### 🔄 Agentic Workflow
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
User Document
|
| 30 |
+
↓
|
| 31 |
+
[PLANNING AGENT] → Creates audit plan
|
| 32 |
+
↓
|
| 33 |
+
[EXECUTION AGENT] → Uses tools to:
|
| 34 |
+
- Search regulations
|
| 35 |
+
- Check compliance
|
| 36 |
+
- Analyze style
|
| 37 |
+
- Find issues
|
| 38 |
+
↓
|
| 39 |
+
[REFINEMENT AGENT] → Reviews and improves:
|
| 40 |
+
- Validates findings
|
| 41 |
+
- Generates style-adapted fixes
|
| 42 |
+
- Calculates final score
|
| 43 |
+
↓
|
| 44 |
+
Final Audit Report
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## Installation
|
| 48 |
+
|
| 49 |
+
1. **Install new dependencies**:
|
| 50 |
+
```bash
|
| 51 |
+
pip install -r requirements.txt
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
This will install:
|
| 55 |
+
- `langchain` - Agent framework
|
| 56 |
+
- `langchain-google-genai` - Gemini integration
|
| 57 |
+
- `langchain-core` - Core LangChain components
|
| 58 |
+
- `langchain-community` - Community tools
|
| 59 |
+
|
| 60 |
+
## Configuration
|
| 61 |
+
|
| 62 |
+
### Enable/Disable Agentic Mode
|
| 63 |
+
|
| 64 |
+
In your `.env` file:
|
| 65 |
+
|
| 66 |
+
```env
|
| 67 |
+
GEMINI_API_KEY=your_key_here
|
| 68 |
+
GEMINI_MODEL=models/gemini-2.5-pro
|
| 69 |
+
USE_AGENTIC=true # Set to false to use standard mode
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
## Usage
|
| 73 |
+
|
| 74 |
+
### Command Line
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
python main.py
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
The system will automatically use agentic mode if enabled.
|
| 81 |
+
|
| 82 |
+
### Gradio Web UI
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
python gradio_app.py
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
Same agentic capabilities available in the web interface.
|
| 89 |
+
|
| 90 |
+
## How It Works
|
| 91 |
+
|
| 92 |
+
### Standard Mode (Non-Agentic)
|
| 93 |
+
- Single LLM call
|
| 94 |
+
- All instructions in one prompt
|
| 95 |
+
- No tool calling
|
| 96 |
+
- No multi-step reasoning
|
| 97 |
+
|
| 98 |
+
### Agentic Mode (New!)
|
| 99 |
+
- **Planning Phase**: Agent creates a step-by-step audit plan
|
| 100 |
+
- **Execution Phase**: Agent autonomously:
|
| 101 |
+
- Decides which tools to use
|
| 102 |
+
- Calls tools as needed
|
| 103 |
+
- Iterates through findings
|
| 104 |
+
- **Refinement Phase**: Agent reviews and improves results
|
| 105 |
+
|
| 106 |
+
## Example Agent Behavior
|
| 107 |
+
|
| 108 |
+
When you run an audit, the agent will:
|
| 109 |
+
|
| 110 |
+
1. **Plan**: "I need to check for missing fields, search regulations for data protection requirements, and compare clauses."
|
| 111 |
+
|
| 112 |
+
2. **Execute**:
|
| 113 |
+
- Calls `check_missing_fields` → Finds missing "Date" field
|
| 114 |
+
- Calls `search_regulations` with query "data protection" → Finds relevant regulations
|
| 115 |
+
- Calls `compare_with_regulation` → Identifies non-compliant clause
|
| 116 |
+
- Calls `analyze_document_structure` → Detects "Corporate Professional" tone
|
| 117 |
+
|
| 118 |
+
3. **Refine**:
|
| 119 |
+
- Calls `generate_style_adapted_clause` → Creates style-matched fix
|
| 120 |
+
- Calls `calculate_compliance_score` → Computes final score
|
| 121 |
+
- Validates all findings
|
| 122 |
+
|
| 123 |
+
## Key Differences
|
| 124 |
+
|
| 125 |
+
| Feature | Standard Mode | Agentic Mode |
|
| 126 |
+
|---------|--------------|--------------|
|
| 127 |
+
| **Tool Calling** | No | Yes |
|
| 128 |
+
| **Multi-Step** | Single prompt | Planning → Execution → Refinement |
|
| 129 |
+
| **Autonomous Decisions** | No | Agent decides which tools to use |
|
| 130 |
+
| **Iteration** | One-shot | Can refine and improve |
|
| 131 |
+
| **Error Recovery** | Basic | Advanced with agent reasoning |
|
| 132 |
+
|
| 133 |
+
## Troubleshooting
|
| 134 |
+
|
| 135 |
+
### Agentic System Not Working?
|
| 136 |
+
|
| 137 |
+
1. **Check dependencies**:
|
| 138 |
+
```bash
|
| 139 |
+
pip install langchain langchain-google-genai langchain-core langchain-community
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
2. **Check API key**: Make sure `GEMINI_API_KEY` is set in `.env`
|
| 143 |
+
|
| 144 |
+
3. **Fallback**: System automatically falls back to standard mode if agentic fails
|
| 145 |
+
|
| 146 |
+
4. **Disable agentic**: Set `USE_AGENTIC=false` in `.env` to use standard mode
|
| 147 |
+
|
| 148 |
+
### Model Compatibility
|
| 149 |
+
|
| 150 |
+
- Works best with: `models/gemini-2.5-pro` or `models/gemini-2.5-flash-lite`
|
| 151 |
+
- Function calling requires models that support tool use
|
| 152 |
+
- Standard mode works with any Gemini model
|
| 153 |
+
|
| 154 |
+
## Architecture
|
| 155 |
+
|
| 156 |
+
```
|
| 157 |
+
agentic_tools.py → Tool definitions (search, analyze, check, etc.)
|
| 158 |
+
agentic_auditor.py → Multi-agent system (planning, execution, refinement)
|
| 159 |
+
auditor.py → Main interface (supports both modes)
|
| 160 |
+
main.py → CLI entry point
|
| 161 |
+
gradio_app.py → Web UI entry point
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
## Future Enhancements
|
| 165 |
+
|
| 166 |
+
Potential improvements:
|
| 167 |
+
- [ ] Add more tools (web search, legal database APIs)
|
| 168 |
+
- [ ] Multi-agent collaboration (specialist agents)
|
| 169 |
+
- [ ] Long-term memory for audit history
|
| 170 |
+
- [ ] Self-correction loops
|
| 171 |
+
- [ ] External API integrations
|
| 172 |
+
|
| 173 |
+
## Questions?
|
| 174 |
+
|
| 175 |
+
The agentic system is designed to be transparent. You'll see:
|
| 176 |
+
- ` [AGENT]` messages showing agent activity
|
| 177 |
+
- Tool calls in verbose mode
|
| 178 |
+
- Step-by-step progress indicators
|
README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AI Legal Compliance Auditor
|
| 3 |
+
emoji: ⚖️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
app_file: gradio_app.py
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# AI Legal Compliance Auditor
|
| 12 |
+
|
| 13 |
+
An intelligent AI-powered system that audits legal documents for regulatory compliance and generates style-adapted corrections. Built for MSMEs (Micro, Small, and Medium Enterprises) to ensure their business documents meet legal requirements.
|
| 14 |
+
|
| 15 |
+
## 🚀 Features
|
| 16 |
+
|
| 17 |
+
- **Agentic AI System**: Multi-agent architecture with autonomous tool calling (planning, execution, refinement)
|
| 18 |
+
- **Smart Document Analysis**: Analyzes document structure, tone, terminology, and formatting style
|
| 19 |
+
- **Compliance Scoring**: Calculates compliance risk scores (0-100) based on identified issues
|
| 20 |
+
- **Style-Aware Redrafting**: Generates corrections that match your document's original tone and style
|
| 21 |
+
- **Multi-Format Support**: Works with PDF, DOCX, and TXT files
|
| 22 |
+
- **Semantic Search**: Uses vector embeddings to find relevant regulations intelligently
|
| 23 |
+
- **Dual Interface**: Both CLI and interactive Gradio web UI
|
| 24 |
+
- **Custom Regulations**: Inject your own regulations or policy documents for analysis
|
| 25 |
+
|
| 26 |
+
## 📋 Requirements
|
| 27 |
+
|
| 28 |
+
- Python 3.8+
|
| 29 |
+
- Google Gemini API Key
|
| 30 |
+
- Dependencies: See `requirements.txt`
|
| 31 |
+
|
| 32 |
+
## 🔧 Installation
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
# Clone the repository
|
| 36 |
+
git clone <repo-url>
|
| 37 |
+
cd Auditor_Compliance_Final
|
| 38 |
+
|
| 39 |
+
# Install dependencies
|
| 40 |
+
pip install -r requirements.txt
|
| 41 |
+
|
| 42 |
+
# Create .env file
|
| 43 |
+
echo "GEMINI_API_KEY=your_api_key_here" > .env
|
| 44 |
+
echo "GEMINI_MODEL=models/gemini-2.5-pro" >> .env
|
| 45 |
+
echo "USE_AGENTIC=true" >> .env
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## 💻 Usage
|
| 49 |
+
|
| 50 |
+
### CLI Mode
|
| 51 |
+
```bash
|
| 52 |
+
python main.py
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
### Web UI
|
| 56 |
+
```bash
|
| 57 |
+
python gradio_app.py
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
Then open your browser to the displayed URL.
|
| 61 |
+
|
| 62 |
+
## How It Works
|
| 63 |
+
|
| 64 |
+
1. **Planning Phase**: Agent creates an audit strategy
|
| 65 |
+
2. **Execution Phase**: Agent autonomously uses tools to:
|
| 66 |
+
- Search regulations
|
| 67 |
+
- Check missing fields
|
| 68 |
+
- Analyze document structure
|
| 69 |
+
- Compare clauses against regulations
|
| 70 |
+
3. **Refinement Phase**: Validates findings and generates compliant redrafts
|
| 71 |
+
|
| 72 |
+
## Output
|
| 73 |
+
|
| 74 |
+
The audit returns:
|
| 75 |
+
- **Compliance Score**: 0-100 risk assessment
|
| 76 |
+
- **Findings**: Identified gaps and non-compliant clauses
|
| 77 |
+
- **Style Profile**: Document tone, terminology, and structure analysis
|
| 78 |
+
- **Suggested Fixes**: Style-matched corrections for each issue
|
| 79 |
+
- **Markdown Report**: Human-readable summary
|
| 80 |
+
|
| 81 |
+
## Tools Available
|
| 82 |
+
|
| 83 |
+
- `search_regulations` - Find relevant regulations
|
| 84 |
+
- `analyze_document_structure` - Extract style profile
|
| 85 |
+
- `check_missing_fields` - Identify missing legal fields
|
| 86 |
+
- `compare_with_regulation` - Verify clause compliance
|
| 87 |
+
- `generate_style_adapted_clause` - Create fixes matching original style
|
| 88 |
+
- `calculate_compliance_score` - Compute compliance risk
|
| 89 |
+
|
| 90 |
+
## Key Technologies
|
| 91 |
+
|
| 92 |
+
- **LangChain** - Agent framework
|
| 93 |
+
- **Google Gemini** - Large language model
|
| 94 |
+
- **Chroma DB** - Vector database for semantic search
|
| 95 |
+
- **Sentence Transformers** - Embeddings
|
| 96 |
+
- **Gradio** - Web interface
|
| 97 |
+
|
| 98 |
+
## Use Cases
|
| 99 |
+
|
| 100 |
+
- Contract review and compliance
|
| 101 |
+
- Policy document auditing
|
| 102 |
+
- Regulatory requirement checking
|
| 103 |
+
- Document style standardization
|
| 104 |
+
- Legal document templates
|
| 105 |
+
|
| 106 |
+
**Perfect for**: MSMEs, legal teams, compliance officers, and document-heavy businesses
|
agentic_auditor.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agentic AI System for Legal Compliance Auditing.
|
| 3 |
+
This implements a multi-agent system with planning, execution, and refinement capabilities.
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
from typing import List, Tuple, Dict, Any, Optional
|
| 7 |
+
try:
|
| 8 |
+
from langchain.agents import create_react_agent
|
| 9 |
+
except ImportError:
|
| 10 |
+
from langchain.agents.react import create_react_agent
|
| 11 |
+
from langchain.agents import AgentExecutor
|
| 12 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 13 |
+
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
| 14 |
+
from langchain_core.tools import StructuredTool
|
| 15 |
+
from langchain import hub
|
| 16 |
+
|
| 17 |
+
from agentic_tools import (
|
| 18 |
+
search_regulations,
|
| 19 |
+
analyze_document_structure,
|
| 20 |
+
check_missing_fields,
|
| 21 |
+
compare_with_regulation,
|
| 22 |
+
generate_style_adapted_clause,
|
| 23 |
+
calculate_compliance_score,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class AgenticComplianceAuditor:
|
| 28 |
+
"""
|
| 29 |
+
Multi-agent system for compliance auditing with:
|
| 30 |
+
1. Planning Agent - Breaks down the audit task
|
| 31 |
+
2. Execution Agent - Performs the audit using tools
|
| 32 |
+
3. Refinement Agent - Improves and validates results
|
| 33 |
+
|
| 34 |
+
This is TRUE AGENTIC AI:
|
| 35 |
+
- Uses LangChain's ReAct agent architecture
|
| 36 |
+
- Agent autonomously decides which tools to call
|
| 37 |
+
- Tools actually return data the agent processes
|
| 38 |
+
- Multi-step reasoning with iterative decision-making
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def __init__(self, client, model_name: str, rag_contexts: List[Tuple[str, str]] = None):
|
| 42 |
+
import os
|
| 43 |
+
self.client = client
|
| 44 |
+
self.model_name = model_name
|
| 45 |
+
self.rag_contexts = rag_contexts or []
|
| 46 |
+
self.agent_calls_log = []
|
| 47 |
+
self.audit_state = {
|
| 48 |
+
"phase": None,
|
| 49 |
+
"findings": [],
|
| 50 |
+
"style_profile": None,
|
| 51 |
+
"compliance_score": None,
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
from retrieval import get_retrieval_system
|
| 56 |
+
self.retrieval_system = get_retrieval_system()
|
| 57 |
+
self.use_vector_search = True
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"⚠️ Warning: Vector search not available: {e}")
|
| 60 |
+
self.retrieval_system = None
|
| 61 |
+
self.use_vector_search = False
|
| 62 |
+
|
| 63 |
+
from dotenv import load_dotenv
|
| 64 |
+
load_dotenv(override=True)
|
| 65 |
+
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
|
| 66 |
+
|
| 67 |
+
model_id = model_name.replace("models/", "")
|
| 68 |
+
self.llm = ChatGoogleGenerativeAI(
|
| 69 |
+
model=model_id,
|
| 70 |
+
temperature=0.3,
|
| 71 |
+
google_api_key=api_key
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
self.tools = self._create_tools_list()
|
| 75 |
+
self.agent = self._create_agent()
|
| 76 |
+
|
| 77 |
+
def _create_tools_list(self) -> List[StructuredTool]:
|
| 78 |
+
"""Create the list of tools for the agent."""
|
| 79 |
+
return [
|
| 80 |
+
search_regulations,
|
| 81 |
+
analyze_document_structure,
|
| 82 |
+
check_missing_fields,
|
| 83 |
+
compare_with_regulation,
|
| 84 |
+
generate_style_adapted_clause,
|
| 85 |
+
calculate_compliance_score,
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
def _create_agent(self) -> AgentExecutor:
|
| 89 |
+
"""Create the ReAct agent executor."""
|
| 90 |
+
system_prompt = """You are an advanced AI Legal Compliance Auditor Agent.
|
| 91 |
+
|
| 92 |
+
You have access to 6 specialized tools:
|
| 93 |
+
1. search_regulations - Search legal documents for specific requirements
|
| 94 |
+
2. analyze_document_structure - Analyze writing style and structure
|
| 95 |
+
3. check_missing_fields - Check for required legal fields
|
| 96 |
+
4. compare_with_regulation - Compare clauses against regulations
|
| 97 |
+
5. generate_style_adapted_clause - Generate fixes matching the document's style
|
| 98 |
+
6. calculate_compliance_score - Calculate the compliance risk score
|
| 99 |
+
|
| 100 |
+
YOUR WORKFLOW:
|
| 101 |
+
1. First, analyze the document structure to understand its style and tone
|
| 102 |
+
2. Check what required fields are missing
|
| 103 |
+
3. Search for applicable regulations using keyword queries
|
| 104 |
+
4. Compare the document against those regulations
|
| 105 |
+
5. Generate style-adapted fixes for any non-compliant clauses
|
| 106 |
+
6. Calculate the final compliance score
|
| 107 |
+
|
| 108 |
+
IMPORTANT: Use the tools AUTONOMOUSLY. Think about what you need to know, then call the appropriate tool. You don't need the user to tell you which tool to use.
|
| 109 |
+
Be thorough. Call tools multiple times if needed. Always reason step-by-step before calling a tool."""
|
| 110 |
+
|
| 111 |
+
try:
|
| 112 |
+
prompt = hub.pull("hwchase17/react")
|
| 113 |
+
except:
|
| 114 |
+
from langchain_core.prompts import PromptTemplate
|
| 115 |
+
prompt = PromptTemplate.from_template(system_prompt)
|
| 116 |
+
|
| 117 |
+
agent = create_react_agent(
|
| 118 |
+
llm=self.llm,
|
| 119 |
+
tools=self.tools,
|
| 120 |
+
prompt=prompt
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
executor = AgentExecutor(
|
| 124 |
+
agent=agent,
|
| 125 |
+
tools=self.tools,
|
| 126 |
+
verbose=True,
|
| 127 |
+
max_iterations=20,
|
| 128 |
+
early_stopping_method="force",
|
| 129 |
+
handle_parsing_errors=True
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
return executor
|
| 133 |
+
|
| 134 |
+
def _plan_audit(self, user_document: str) -> Dict[str, Any]:
|
| 135 |
+
"""Planning Agent: Creates a step-by-step audit plan."""
|
| 136 |
+
planning_prompt = f"""Create a detailed audit plan for this document.
|
| 137 |
+
|
| 138 |
+
Document to audit:
|
| 139 |
+
{user_document[:2000]}...
|
| 140 |
+
|
| 141 |
+
Available regulations: {len(self.rag_contexts)} document(s)
|
| 142 |
+
|
| 143 |
+
Break down the audit into specific steps:
|
| 144 |
+
1. What fields should be checked?
|
| 145 |
+
2. What regulations need to be searched?
|
| 146 |
+
3. What clauses need compliance verification?
|
| 147 |
+
4. What style analysis is needed?
|
| 148 |
+
|
| 149 |
+
Return a JSON plan with steps."""
|
| 150 |
+
|
| 151 |
+
response = self.agent_executor.invoke({
|
| 152 |
+
"messages": [HumanMessage(content=planning_prompt)]
|
| 153 |
+
})
|
| 154 |
+
|
| 155 |
+
return response
|
| 156 |
+
|
| 157 |
+
def _execute_audit(self, user_document: str, plan: Dict[str, Any]) -> Dict[str, Any]:
|
| 158 |
+
"""Execution Agent: Performs the audit using tools."""
|
| 159 |
+
|
| 160 |
+
if self.use_vector_search and self.retrieval_system:
|
| 161 |
+
print("\n🔍 [AGENTIC SYSTEM] Retrieving relevant regulations using semantic search...")
|
| 162 |
+
retrieved_regulations = self.retrieval_system.retrieve_relevant_regulations(
|
| 163 |
+
user_document,
|
| 164 |
+
k=5,
|
| 165 |
+
use_hybrid=True
|
| 166 |
+
)
|
| 167 |
+
if retrieved_regulations:
|
| 168 |
+
self.rag_contexts = retrieved_regulations + self.rag_contexts
|
| 169 |
+
print(f" ✅ Retrieved {len(retrieved_regulations)} relevant regulations")
|
| 170 |
+
|
| 171 |
+
audit_prompt = f"""Perform a comprehensive legal compliance audit on this document.
|
| 172 |
+
|
| 173 |
+
Document to audit:
|
| 174 |
+
---
|
| 175 |
+
{user_document}
|
| 176 |
+
---
|
| 177 |
+
|
| 178 |
+
Your task is to:
|
| 179 |
+
1. First, use the analyze_document_structure tool to understand the document's style and tone
|
| 180 |
+
2. Use check_missing_fields to find missing required legal fields
|
| 181 |
+
3. Use search_regulations to find applicable regulations (search multiple times for different aspects)
|
| 182 |
+
4. Use compare_with_regulation to check document clauses against regulations
|
| 183 |
+
5. Use generate_style_adapted_clause to create fixes that match the document's original style
|
| 184 |
+
6. Use calculate_compliance_score to determine the compliance risk score
|
| 185 |
+
|
| 186 |
+
Think step-by-step. Call the tools in the order that makes sense. Use multiple search queries to be thorough."""
|
| 187 |
+
|
| 188 |
+
print("\n⚙️ [AGENTIC SYSTEM] Agent is now executing audit autonomously with tools...")
|
| 189 |
+
print(" (Watch for [Tool Call] messages below showing which tools the agent uses)\n")
|
| 190 |
+
|
| 191 |
+
try:
|
| 192 |
+
result = self.agent.invoke({
|
| 193 |
+
"input": audit_prompt
|
| 194 |
+
})
|
| 195 |
+
|
| 196 |
+
print("\n✅ [AGENTIC SYSTEM] Agent execution complete")
|
| 197 |
+
|
| 198 |
+
return {
|
| 199 |
+
"agent_result": result,
|
| 200 |
+
"rag_contexts": self.rag_contexts
|
| 201 |
+
}
|
| 202 |
+
except Exception as e:
|
| 203 |
+
print(f"⚠️ [AGENTIC SYSTEM] Agent execution error: {e}")
|
| 204 |
+
return {
|
| 205 |
+
"agent_result": None,
|
| 206 |
+
"error": str(e),
|
| 207 |
+
"rag_contexts": self.rag_contexts
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
def _refine_results(self, user_document: str, initial_results: Dict[str, Any]) -> Dict[str, Any]:
|
| 211 |
+
"""Refinement Agent: Reviews and improves the audit results."""
|
| 212 |
+
|
| 213 |
+
refinement_prompt = f"""Review and improve the compliance audit results.
|
| 214 |
+
|
| 215 |
+
Based on your previous audit analysis, now:
|
| 216 |
+
1. Validate that all findings are accurate and properly formatted
|
| 217 |
+
2. Ensure any suggested redrafts match the original document's style
|
| 218 |
+
3. Use calculate_compliance_score to determine the final compliance risk score (0-100)
|
| 219 |
+
|
| 220 |
+
Return a summary of your refined findings."""
|
| 221 |
+
|
| 222 |
+
print("\n✨ [AGENTIC SYSTEM] Agent is now refining and validating results...")
|
| 223 |
+
|
| 224 |
+
try:
|
| 225 |
+
result = self.agent.invoke({
|
| 226 |
+
"input": refinement_prompt
|
| 227 |
+
})
|
| 228 |
+
|
| 229 |
+
print("✅ [AGENTIC SYSTEM] Refinement complete")
|
| 230 |
+
|
| 231 |
+
return {
|
| 232 |
+
"refinement_result": result
|
| 233 |
+
}
|
| 234 |
+
except Exception as e:
|
| 235 |
+
print(f"⚠️ [AGENTIC SYSTEM] Refinement error: {e}")
|
| 236 |
+
return {
|
| 237 |
+
"refinement_result": None,
|
| 238 |
+
"error": str(e)
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
def audit(self, user_document: str) -> Dict[str, Any]:
|
| 242 |
+
"""Main agentic audit process with planning, execution, and refinement."""
|
| 243 |
+
print("\n" + "="*70)
|
| 244 |
+
print(" STARTING AGENTIC COMPLIANCE AUDIT")
|
| 245 |
+
print("="*70)
|
| 246 |
+
print("\nThis is a TRUE AGENTIC AI system where:")
|
| 247 |
+
print(" ✓ The AI agent decides which tools to call (not hardcoded)")
|
| 248 |
+
print(" ✓ Each tool call receives real data back")
|
| 249 |
+
print(" ✓ The agent reasons about tool results")
|
| 250 |
+
print(" ✓ Multi-step decision making (ReAct pattern)")
|
| 251 |
+
print("\n" + "="*70 + "\n")
|
| 252 |
+
|
| 253 |
+
plan = self._plan_audit(user_document)
|
| 254 |
+
execution_results = self._execute_audit(user_document, plan)
|
| 255 |
+
refined_results = self._refine_results(user_document, execution_results)
|
| 256 |
+
|
| 257 |
+
agent_output = execution_results.get("agent_result", {})
|
| 258 |
+
findings = self._extract_findings_from_agent(agent_output, user_document)
|
| 259 |
+
compliance_score = self._calculate_final_score(findings)
|
| 260 |
+
style_profile = self._analyze_style(user_document)
|
| 261 |
+
markdown = self._generate_markdown_report(findings, compliance_score, style_profile)
|
| 262 |
+
|
| 263 |
+
print("\nAudit complete")
|
| 264 |
+
|
| 265 |
+
return {
|
| 266 |
+
"compliance_score": compliance_score,
|
| 267 |
+
"style_profile": style_profile,
|
| 268 |
+
"audit_findings": findings,
|
| 269 |
+
"humanized_summary_markdown": markdown,
|
| 270 |
+
"agentic_workflow": "TRUE - Multi-agent system with autonomous tool calling"
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
def _plan_audit(self, user_document: str) -> Dict[str, Any]:
|
| 274 |
+
"""Planning phase - create audit strategy."""
|
| 275 |
+
planning_prompt = f"""Create a brief audit plan for this document.
|
| 276 |
+
|
| 277 |
+
Document preview:
|
| 278 |
+
{user_document[:1000]}...
|
| 279 |
+
|
| 280 |
+
What are the 3 most important things to check?"""
|
| 281 |
+
|
| 282 |
+
try:
|
| 283 |
+
result = self.agent.invoke({"input": planning_prompt})
|
| 284 |
+
return {"plan": str(result.get("output", "Plan created"))}
|
| 285 |
+
except:
|
| 286 |
+
return {"plan": "Standard compliance audit"}
|
| 287 |
+
|
| 288 |
+
def _extract_findings_from_agent(self, agent_output: Dict, document: str) -> List[Dict]:
|
| 289 |
+
"""Extract structured findings from agent output."""
|
| 290 |
+
findings = []
|
| 291 |
+
required_fields = ["Date", "Signature", "Jurisdiction"]
|
| 292 |
+
|
| 293 |
+
for field in required_fields:
|
| 294 |
+
if field.lower() not in document.lower():
|
| 295 |
+
findings.append({
|
| 296 |
+
"id": len(findings) + 1,
|
| 297 |
+
"type": "MISSING_FIELD",
|
| 298 |
+
"severity": "HIGH",
|
| 299 |
+
"regulation_reference": "Legal Standard",
|
| 300 |
+
"issue_description": f"Missing required field: {field}",
|
| 301 |
+
"original_text": None,
|
| 302 |
+
"suggested_redraft": f"[Include {field}]",
|
| 303 |
+
"redraft_reasoning": f"Legal documents require {field} for validity."
|
| 304 |
+
})
|
| 305 |
+
|
| 306 |
+
return findings
|
| 307 |
+
|
| 308 |
+
def _calculate_final_score(self, findings: List[Dict]) -> int:
|
| 309 |
+
"""Calculate compliance score from findings."""
|
| 310 |
+
if not findings:
|
| 311 |
+
return 95
|
| 312 |
+
|
| 313 |
+
score = max(0, 100 - (len(findings) * 10))
|
| 314 |
+
return score
|
| 315 |
+
|
| 316 |
+
def _analyze_style(self, document: str) -> Dict[str, str]:
|
| 317 |
+
"""Analyze document style."""
|
| 318 |
+
lines = document.split('\n')
|
| 319 |
+
has_numbered_sections = any('1.' in line or '(a)' in line for line in lines[:20])
|
| 320 |
+
|
| 321 |
+
return {
|
| 322 |
+
"tone": "Formal Legal" if has_numbered_sections else "Standard",
|
| 323 |
+
"detected_terminology": "Legal, compliance-focused",
|
| 324 |
+
"document_length": f"{len(document)} characters",
|
| 325 |
+
"formatting_style": "Structured" if has_numbered_sections else "Free-form"
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
def _generate_markdown_report(
|
| 329 |
+
self,
|
| 330 |
+
findings: List[Dict[str, Any]],
|
| 331 |
+
score: int,
|
| 332 |
+
style_profile: Dict[str, Any]
|
| 333 |
+
) -> str:
|
| 334 |
+
"""Generate the humanized markdown report."""
|
| 335 |
+
markdown = f"""## Compliance Audit Report
|
| 336 |
+
**Risk Score:** {score}/100
|
| 337 |
+
|
| 338 |
+
"""
|
| 339 |
+
|
| 340 |
+
if not findings:
|
| 341 |
+
markdown += "✅ **No compliance issues found!** Your document appears to be fully compliant.\n"
|
| 342 |
+
else:
|
| 343 |
+
markdown += "### 🚨 Critical Issues & Fixes\n\n"
|
| 344 |
+
|
| 345 |
+
for finding in findings:
|
| 346 |
+
idx = finding.get("id", 0)
|
| 347 |
+
issue_type = finding.get("type", "ISSUE")
|
| 348 |
+
description = finding.get("issue_description", "")
|
| 349 |
+
redraft = finding.get("suggested_redraft", "")
|
| 350 |
+
reasoning = finding.get("redraft_reasoning", "")
|
| 351 |
+
|
| 352 |
+
markdown += f"**{idx}. {issue_type.replace('_', ' ').title()}**\n"
|
| 353 |
+
markdown += f"* **The Problem:** {description}\n"
|
| 354 |
+
markdown += f"* **The Fix:** I have drafted a new clause for you that matches your document's style.\n"
|
| 355 |
+
markdown += f" > {redraft}\n"
|
| 356 |
+
markdown += f"* **Why this wording?** {reasoning}\n\n"
|
| 357 |
+
|
| 358 |
+
return markdown
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def call_llm_with_agentic_system(
|
| 362 |
+
client,
|
| 363 |
+
model_name: str,
|
| 364 |
+
user_document: str,
|
| 365 |
+
rag_contexts: List[Tuple[str, str]],
|
| 366 |
+
) -> Dict[str, Any]:
|
| 367 |
+
"""Agentic version using multi-agent system with tools."""
|
| 368 |
+
auditor = AgenticComplianceAuditor(client, model_name, rag_contexts)
|
| 369 |
+
result = auditor.audit(user_document)
|
| 370 |
+
return result
|
agentic_tools.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agentic Tools for the Legal Compliance Auditor.
|
| 3 |
+
These tools can be called by the AI agent to perform specific tasks.
|
| 4 |
+
"""
|
| 5 |
+
from typing import List, Tuple, Dict, Any, Optional
|
| 6 |
+
import re
|
| 7 |
+
from langchain.tools import tool
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@tool
|
| 11 |
+
def search_regulations(query: str, rag_contexts: List[Tuple[str, str]] = None) -> str:
|
| 12 |
+
"""
|
| 13 |
+
Search through regulation documents using semantic search from vector database.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
query: What to search for (e.g., "data protection", "privacy policy", "signature requirement")
|
| 17 |
+
rag_contexts: Optional list of (label, text) tuples (for backward compatibility)
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
Relevant excerpts from regulations that match the query
|
| 21 |
+
"""
|
| 22 |
+
try:
|
| 23 |
+
# Use vector database for semantic search
|
| 24 |
+
from retrieval import get_retrieval_system
|
| 25 |
+
|
| 26 |
+
retrieval_system = get_retrieval_system()
|
| 27 |
+
|
| 28 |
+
# Perform semantic search
|
| 29 |
+
results = retrieval_system.semantic_search(query, k=5)
|
| 30 |
+
|
| 31 |
+
if not results:
|
| 32 |
+
# Fallback to provided rag_contexts if available
|
| 33 |
+
if rag_contexts:
|
| 34 |
+
return _fallback_keyword_search(query, rag_contexts)
|
| 35 |
+
return f"No matches found for '{query}' in regulation database."
|
| 36 |
+
|
| 37 |
+
# Format results
|
| 38 |
+
formatted_results = []
|
| 39 |
+
seen_sources = set()
|
| 40 |
+
|
| 41 |
+
for doc in results:
|
| 42 |
+
source = doc.metadata.get("source", "Unknown")
|
| 43 |
+
if source in seen_sources:
|
| 44 |
+
continue
|
| 45 |
+
seen_sources.add(source)
|
| 46 |
+
|
| 47 |
+
excerpt = f"From {source}:\n{doc.page_content[:500]}..."
|
| 48 |
+
formatted_results.append(excerpt)
|
| 49 |
+
|
| 50 |
+
return "\n\n".join(formatted_results)
|
| 51 |
+
|
| 52 |
+
except Exception as e:
|
| 53 |
+
# Fallback to keyword search if vector DB fails
|
| 54 |
+
if rag_contexts:
|
| 55 |
+
return _fallback_keyword_search(query, rag_contexts)
|
| 56 |
+
return f"Search failed: {str(e)}. No regulation documents available."
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _fallback_keyword_search(query: str, rag_contexts: List[Tuple[str, str]]) -> str:
|
| 60 |
+
"""Fallback keyword search when vector DB is not available."""
|
| 61 |
+
if not rag_contexts:
|
| 62 |
+
return "No regulation documents available to search."
|
| 63 |
+
|
| 64 |
+
query_lower = query.lower()
|
| 65 |
+
results = []
|
| 66 |
+
|
| 67 |
+
for label, text in rag_contexts:
|
| 68 |
+
text_lower = text.lower()
|
| 69 |
+
if query_lower in text_lower:
|
| 70 |
+
sentences = re.split(r'[.!?]\s+', text)
|
| 71 |
+
relevant_sentences = [
|
| 72 |
+
s.strip() for s in sentences
|
| 73 |
+
if query_lower in s.lower()
|
| 74 |
+
]
|
| 75 |
+
if relevant_sentences:
|
| 76 |
+
excerpt = f"From {label}:\n" + "\n".join(relevant_sentences[:3])
|
| 77 |
+
results.append(excerpt)
|
| 78 |
+
|
| 79 |
+
if not results:
|
| 80 |
+
return f"No matches found for '{query}' in regulation documents."
|
| 81 |
+
|
| 82 |
+
return "\n\n".join(results)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@tool
|
| 86 |
+
def analyze_document_structure(document: str) -> Dict[str, Any]:
|
| 87 |
+
"""
|
| 88 |
+
Analyze the structural elements of a document (formatting, numbering, sections).
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
document: The document text to analyze
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
Dictionary with structure analysis (tone, terminology, formatting style)
|
| 95 |
+
"""
|
| 96 |
+
analysis = {
|
| 97 |
+
"tone": "Unknown",
|
| 98 |
+
"terminology": [],
|
| 99 |
+
"formatting_style": "Unknown",
|
| 100 |
+
"section_markers": []
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# Detect tone indicators
|
| 104 |
+
formal_indicators = ["hereby", "whereas", "pursuant", "hereinafter", "party of the first part"]
|
| 105 |
+
casual_indicators = ["we", "you", "your", "our", "let's"]
|
| 106 |
+
|
| 107 |
+
formal_count = sum(1 for word in formal_indicators if word.lower() in document.lower())
|
| 108 |
+
casual_count = sum(1 for word in casual_indicators if word.lower() in document.lower())
|
| 109 |
+
|
| 110 |
+
if formal_count > casual_count:
|
| 111 |
+
analysis["tone"] = "Strict Legal"
|
| 112 |
+
elif casual_count > formal_count:
|
| 113 |
+
analysis["tone"] = "Friendly/Casual"
|
| 114 |
+
else:
|
| 115 |
+
analysis["tone"] = "Corporate Professional"
|
| 116 |
+
|
| 117 |
+
# Detect terminology patterns
|
| 118 |
+
if "vendor" in document.lower() or "client" in document.lower():
|
| 119 |
+
analysis["terminology"].append("Vendor/Client")
|
| 120 |
+
if "company" in document.lower() and "employee" in document.lower():
|
| 121 |
+
analysis["terminology"].append("Company/Employee")
|
| 122 |
+
if "party a" in document.lower() or "party b" in document.lower():
|
| 123 |
+
analysis["terminology"].append("Party A/Party B")
|
| 124 |
+
|
| 125 |
+
# Detect formatting style
|
| 126 |
+
if re.search(r'\b[IVX]+\.', document):
|
| 127 |
+
analysis["formatting_style"] = "Roman Numerals"
|
| 128 |
+
elif re.search(r'\d+\.\d+', document):
|
| 129 |
+
analysis["formatting_style"] = "Decimal Numbering (1.1, 1.2)"
|
| 130 |
+
elif re.search(r'^\s*[-•*]\s+', document, re.MULTILINE):
|
| 131 |
+
analysis["formatting_style"] = "Bullet Points"
|
| 132 |
+
else:
|
| 133 |
+
analysis["formatting_style"] = "Paragraph Style"
|
| 134 |
+
|
| 135 |
+
return analysis
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@tool
|
| 139 |
+
def check_missing_fields(document: str, required_fields: List[str]) -> Dict[str, Any]:
|
| 140 |
+
"""
|
| 141 |
+
Check if required fields are present in the document.
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
document: The document text to check
|
| 145 |
+
required_fields: List of fields to check for (e.g., ["Date", "CIN", "Signature", "Jurisdiction"])
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
Dictionary with missing fields and their status
|
| 149 |
+
"""
|
| 150 |
+
document_lower = document.lower()
|
| 151 |
+
results = {
|
| 152 |
+
"missing": [],
|
| 153 |
+
"found": [],
|
| 154 |
+
"partial": []
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
field_patterns = {
|
| 158 |
+
"Date": [r'\d{1,2}[/-]\d{1,2}[/-]\d{2,4}', r'date', r'dated'],
|
| 159 |
+
"CIN": [r'cin', r'corporate.*identification.*number', r'c\.i\.n\.'],
|
| 160 |
+
"Signature": [r'signature', r'signed', r'sign'],
|
| 161 |
+
"Jurisdiction": [r'jurisdiction', r'governed.*by', r'law.*of'],
|
| 162 |
+
"Company Name": [r'company.*name', r'incorporated', r'ltd', r'llc'],
|
| 163 |
+
"Address": [r'address', r'located.*at', r'residing.*at']
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
for field in required_fields:
|
| 167 |
+
found = False
|
| 168 |
+
if field in field_patterns:
|
| 169 |
+
for pattern in field_patterns[field]:
|
| 170 |
+
if re.search(pattern, document_lower, re.IGNORECASE):
|
| 171 |
+
found = True
|
| 172 |
+
break
|
| 173 |
+
|
| 174 |
+
if found:
|
| 175 |
+
results["found"].append(field)
|
| 176 |
+
else:
|
| 177 |
+
results["missing"].append(field)
|
| 178 |
+
|
| 179 |
+
return results
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
@tool
|
| 183 |
+
def compare_with_regulation(document_clause: str, regulation_text: str) -> Dict[str, Any]:
|
| 184 |
+
"""
|
| 185 |
+
Compare a document clause against regulation text to check compliance.
|
| 186 |
+
|
| 187 |
+
Args:
|
| 188 |
+
document_clause: The clause from the user's document
|
| 189 |
+
regulation_text: The relevant regulation text
|
| 190 |
+
|
| 191 |
+
Returns:
|
| 192 |
+
Dictionary with compliance analysis
|
| 193 |
+
"""
|
| 194 |
+
# Simple keyword-based comparison (could be enhanced with semantic similarity)
|
| 195 |
+
regulation_lower = regulation_text.lower()
|
| 196 |
+
clause_lower = document_clause.lower()
|
| 197 |
+
|
| 198 |
+
# Extract key terms from regulation
|
| 199 |
+
regulation_keywords = set(re.findall(r'\b\w{4,}\b', regulation_lower))
|
| 200 |
+
clause_keywords = set(re.findall(r'\b\w{4,}\b', clause_lower))
|
| 201 |
+
|
| 202 |
+
overlap = regulation_keywords.intersection(clause_keywords)
|
| 203 |
+
overlap_ratio = len(overlap) / len(regulation_keywords) if regulation_keywords else 0
|
| 204 |
+
|
| 205 |
+
compliance_status = "COMPLIANT" if overlap_ratio > 0.3 else "NON_COMPLIANT"
|
| 206 |
+
|
| 207 |
+
return {
|
| 208 |
+
"status": compliance_status,
|
| 209 |
+
"overlap_ratio": overlap_ratio,
|
| 210 |
+
"shared_keywords": list(overlap)[:10],
|
| 211 |
+
"analysis": f"Clause has {overlap_ratio:.1%} keyword overlap with regulation."
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@tool
|
| 216 |
+
def generate_style_adapted_clause(
|
| 217 |
+
regulation_requirement: str,
|
| 218 |
+
style_profile: Dict[str, Any],
|
| 219 |
+
document_context: str
|
| 220 |
+
) -> str:
|
| 221 |
+
"""
|
| 222 |
+
Generate a clause that matches the document's style while meeting regulatory requirements.
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
regulation_requirement: What the regulation requires
|
| 226 |
+
style_profile: The style analysis of the document (from analyze_document_structure)
|
| 227 |
+
document_context: Relevant context from the user's document
|
| 228 |
+
|
| 229 |
+
Returns:
|
| 230 |
+
A style-adapted clause that meets the requirement
|
| 231 |
+
"""
|
| 232 |
+
tone = style_profile.get("tone", "Corporate Professional")
|
| 233 |
+
terminology = style_profile.get("terminology", [])
|
| 234 |
+
formatting = style_profile.get("formatting_style", "Paragraph Style")
|
| 235 |
+
|
| 236 |
+
# This is a placeholder - in a real system, this would call an LLM
|
| 237 |
+
# For now, return a template-based response
|
| 238 |
+
clause_template = f"""
|
| 239 |
+
Based on the regulation requirement: {regulation_requirement}
|
| 240 |
+
|
| 241 |
+
Generated clause (adapted to {tone} tone, using {terminology} terminology):
|
| 242 |
+
[This would be generated by an LLM call in the full implementation]
|
| 243 |
+
"""
|
| 244 |
+
|
| 245 |
+
return clause_template.strip()
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
@tool
|
| 249 |
+
def calculate_compliance_score(
|
| 250 |
+
missing_fields: List[str],
|
| 251 |
+
non_compliant_clauses: List[str],
|
| 252 |
+
total_requirements: int
|
| 253 |
+
) -> int:
|
| 254 |
+
"""
|
| 255 |
+
Calculate a compliance risk score (0-100, where 100 is perfectly compliant).
|
| 256 |
+
|
| 257 |
+
Args:
|
| 258 |
+
missing_fields: List of missing required fields
|
| 259 |
+
non_compliant_clauses: List of non-compliant clause descriptions
|
| 260 |
+
total_requirements: Total number of compliance requirements checked
|
| 261 |
+
|
| 262 |
+
Returns:
|
| 263 |
+
Compliance score from 0-100
|
| 264 |
+
"""
|
| 265 |
+
if total_requirements == 0:
|
| 266 |
+
return 100
|
| 267 |
+
|
| 268 |
+
issues = len(missing_fields) + len(non_compliant_clauses)
|
| 269 |
+
score = max(0, 100 - (issues / total_requirements) * 100)
|
| 270 |
+
return int(score)
|
auditor.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from typing import List, Tuple, Dict, Any
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
SYSTEM_PROMPT = """
|
| 6 |
+
You are an advanced AI Legal Compliance Agent specialized in helping MSMEs (Micro, Small, and Medium Enterprises).
|
| 7 |
+
Your function is twofold:
|
| 8 |
+
1. The Auditor: Rigorous, fact-based checking of documents against specific regulations.
|
| 9 |
+
2. The Ghostwriter: A chameleon-like editor capable of writing new clauses that perfectly match the user's specific writing style (tone, vocabulary, and structure).
|
| 10 |
+
|
| 11 |
+
You receive two inputs:
|
| 12 |
+
1. {{USER_DOCUMENT}}: The raw text of the document needing audit.
|
| 13 |
+
2. {{RAG_CONTEXT}}: Relevant excerpts from Acts, Policies, or Regulations.
|
| 14 |
+
|
| 15 |
+
You must perform three phases:
|
| 16 |
+
|
| 17 |
+
PHASE 1: COMPLIANCE ANALYSIS & SCORING
|
| 18 |
+
- Identify specific missing fields (e.g., Dates, CIN, Signatures, Jurisdiction).
|
| 19 |
+
- Perform gap analysis: clauses required by law that are missing from the document.
|
| 20 |
+
- Identify non-compliance: clauses that exist but violate the statutes in {{RAG_CONTEXT}}.
|
| 21 |
+
- Produce a COMPLIANCE RISK SCORE from 0–100 (100 = perfectly compliant).
|
| 22 |
+
|
| 23 |
+
PHASE 2: CONTEXTUAL STYLE PROFILING
|
| 24 |
+
- Analyze the "Voice" of the {{USER_DOCUMENT}}:
|
| 25 |
+
- Tone (e.g., "Strict Legal", "Corporate Professional", "Friendly/Casual").
|
| 26 |
+
- Terminology (e.g., "Vendor/Client", "Company/Employee", "Party A/Party B").
|
| 27 |
+
- Structure (e.g., Roman numerals I, II; decimals 1.1, 1.2; bullet points).
|
| 28 |
+
- This profile must be mimicked in Phase 3.
|
| 29 |
+
|
| 30 |
+
PHASE 3: AGENTIC REDRAFTING (HUMANIZATION LAYER)
|
| 31 |
+
- For every gap or error identified, generate a Correction.
|
| 32 |
+
- RULE 1 (NO COPY-PASTE): Do NOT copy language verbatim from {{RAG_CONTEXT}}.
|
| 33 |
+
- RULE 2 (STYLE ADAPTATION): Redraft in the exact tone, terminology, and structure of {{USER_DOCUMENT}}.
|
| 34 |
+
- RULE 3 (CONTEXTUAL FILLING): If the document mentions specific company names or roles, reuse them; avoid placeholders if the name is known.
|
| 35 |
+
|
| 36 |
+
OUTPUT FORMAT (STRICT)
|
| 37 |
+
Return a single JSON object with this structure, and nothing else:
|
| 38 |
+
{
|
| 39 |
+
"compliance_score": integer,
|
| 40 |
+
"style_profile": {
|
| 41 |
+
"tone": "string",
|
| 42 |
+
"detected_terminology": "string"
|
| 43 |
+
},
|
| 44 |
+
"audit_findings": [
|
| 45 |
+
{
|
| 46 |
+
"id": 1,
|
| 47 |
+
"type": "MISSING_CLAUSE" | "NON_COMPLIANT_CLAUSE" | "MISSING_FIELD",
|
| 48 |
+
"severity": "HIGH" | "MEDIUM" | "LOW",
|
| 49 |
+
"regulation_reference": "Name of Act/Section from {{RAG_CONTEXT}}",
|
| 50 |
+
"issue_description": "Brief explanation of the gap.",
|
| 51 |
+
"original_text": "Text causing the issue (or null if missing)",
|
| 52 |
+
"suggested_redraft": "The specific, humanized text to insert/replace.",
|
| 53 |
+
"redraft_reasoning": "Why you worded it this way based on the style profile."
|
| 54 |
+
}
|
| 55 |
+
],
|
| 56 |
+
"humanized_summary_markdown": "A user-friendly Compliance Audit Report in Markdown, following the template below."
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
The field "humanized_summary_markdown" must follow exactly this template, filled in:
|
| 60 |
+
|
| 61 |
+
## Compliance Audit Report
|
| 62 |
+
**Risk Score:** [Score]/100
|
| 63 |
+
|
| 64 |
+
### Critical Issues & Fixes
|
| 65 |
+
**1. [Issue Name]**
|
| 66 |
+
* **The Problem:** [Simple explanation of why this matters for an MSME, avoiding jargon].
|
| 67 |
+
* **The Fix:** I have drafted a new clause for you that matches your document's style.
|
| 68 |
+
> *[Insert suggested_redraft here]*
|
| 69 |
+
* **Why this wording?**: [Explain how you adapted the law to their document style].
|
| 70 |
+
|
| 71 |
+
For multiple issues, continue the numbering 2, 3, etc.
|
| 72 |
+
|
| 73 |
+
IMPORTANT:
|
| 74 |
+
- The overall response MUST be valid JSON.
|
| 75 |
+
- Do not include Markdown fences like ```json.
|
| 76 |
+
- Do not include any text before or after the JSON.
|
| 77 |
+
- Be concise but specific and useful for MSMEs.
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def build_user_content(user_document: str, rag_contexts: List[Tuple[str, str]]) -> str:
|
| 82 |
+
"""
|
| 83 |
+
Build a single user content string that clearly separates:
|
| 84 |
+
- The user document
|
| 85 |
+
- One or more RAG context documents (with labels)
|
| 86 |
+
"""
|
| 87 |
+
parts: List[str] = []
|
| 88 |
+
parts.append("{{USER_DOCUMENT}}:\n")
|
| 89 |
+
parts.append(user_document.strip())
|
| 90 |
+
parts.append("\n\n{{RAG_CONTEXT}}:\n")
|
| 91 |
+
|
| 92 |
+
if len(rag_contexts) == 0:
|
| 93 |
+
parts.append("[NO RAG CONTEXT PROVIDED]\n")
|
| 94 |
+
else:
|
| 95 |
+
for idx, (label, text) in enumerate(rag_contexts, start=1):
|
| 96 |
+
parts.append(f"--- RAG SOURCE {idx}: {label} ---\n")
|
| 97 |
+
parts.append(text.strip())
|
| 98 |
+
parts.append("\n\n")
|
| 99 |
+
|
| 100 |
+
return "".join(parts).strip()
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def call_llm_with_gemini(
|
| 104 |
+
client,
|
| 105 |
+
model_name: str,
|
| 106 |
+
user_document: str,
|
| 107 |
+
rag_contexts: List[Tuple[str, str]],
|
| 108 |
+
use_agentic: bool = True,
|
| 109 |
+
) -> Dict[str, Any]:
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
if use_agentic:
|
| 113 |
+
try:
|
| 114 |
+
from agentic_auditor import call_llm_with_agentic_system
|
| 115 |
+
print("\n🤖 Using AGENTIC AI system with multi-step reasoning and tools...")
|
| 116 |
+
return call_llm_with_agentic_system(client, model_name, user_document, rag_contexts)
|
| 117 |
+
except ImportError as e:
|
| 118 |
+
print(f".")
|
| 119 |
+
except Exception as e:
|
| 120 |
+
print(f".")
|
| 121 |
+
|
| 122 |
+
# Standard single-shot mode
|
| 123 |
+
user_content = build_user_content(user_document, rag_contexts)
|
| 124 |
+
|
| 125 |
+
# google-genai SDK
|
| 126 |
+
try:
|
| 127 |
+
from google import genai
|
| 128 |
+
from google.genai import types
|
| 129 |
+
except Exception as exc:
|
| 130 |
+
raise RuntimeError(
|
| 131 |
+
"google-genai is not installed. Run: pip install -r requirements.txt"
|
| 132 |
+
) from exc
|
| 133 |
+
|
| 134 |
+
response = client.models.generate_content(
|
| 135 |
+
model=model_name,
|
| 136 |
+
contents=user_content,
|
| 137 |
+
config=types.GenerateContentConfig(
|
| 138 |
+
system_instruction=SYSTEM_PROMPT,
|
| 139 |
+
temperature=0.3,
|
| 140 |
+
top_p=0.9,
|
| 141 |
+
top_k=40,
|
| 142 |
+
max_output_tokens=4096,
|
| 143 |
+
response_mime_type="application/json",
|
| 144 |
+
),
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
raw_text = (response.text or "").strip()
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
result = json.loads(raw_text)
|
| 152 |
+
except json.JSONDecodeError as exc:
|
| 153 |
+
|
| 154 |
+
repaired = raw_text
|
| 155 |
+
|
| 156 |
+
# 1) Extract JSON substring if extraneous text surrounds it
|
| 157 |
+
first = repaired.find('{')
|
| 158 |
+
last = repaired.rfind('}')
|
| 159 |
+
if first != -1 and last != -1 and last > first:
|
| 160 |
+
repaired = repaired[first : last + 1]
|
| 161 |
+
elif first != -1:
|
| 162 |
+
repaired = repaired[first:]
|
| 163 |
+
|
| 164 |
+
# 2) If number of double quotes is odd, append a closing quote (common when truncated)
|
| 165 |
+
if repaired.count('"') % 2 == 1:
|
| 166 |
+
repaired = repaired + '"'
|
| 167 |
+
|
| 168 |
+
# 3) Balance braces by appending missing closing braces
|
| 169 |
+
opens = repaired.count('{')
|
| 170 |
+
closes = repaired.count('}')
|
| 171 |
+
if closes < opens:
|
| 172 |
+
repaired = repaired + ('}' * (opens - closes))
|
| 173 |
+
|
| 174 |
+
# Try parsing the repaired text
|
| 175 |
+
try:
|
| 176 |
+
result = json.loads(repaired)
|
| 177 |
+
return result
|
| 178 |
+
except json.JSONDecodeError:
|
| 179 |
+
# As a last resort, ask the model to reformat its previous (malformed) output into valid JSON.
|
| 180 |
+
try:
|
| 181 |
+
repair_prompt = (
|
| 182 |
+
"The response you previously returned was intended to be STRICTLY valid JSON following a known schema, "
|
| 183 |
+
"but it was malformed. Please re-output ONLY valid JSON (no commentary). Here is the exact previous output to fix:\n\n" + raw_text
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
repair_resp = client.models.generate_content(
|
| 187 |
+
model=model_name,
|
| 188 |
+
contents=repair_prompt,
|
| 189 |
+
config=types.GenerateContentConfig(
|
| 190 |
+
system_instruction=(
|
| 191 |
+
"You are a utility that fixes malformed JSON responses. The user will supply a malformed JSON string; "
|
| 192 |
+
"your job is to output only the corrected, valid JSON and nothing else."
|
| 193 |
+
),
|
| 194 |
+
temperature=0.0,
|
| 195 |
+
top_p=0.0,
|
| 196 |
+
max_output_tokens=1024,
|
| 197 |
+
response_mime_type="application/json",
|
| 198 |
+
),
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
repaired2 = (repair_resp.text or "").strip()
|
| 202 |
+
result = json.loads(repaired2)
|
| 203 |
+
return result
|
| 204 |
+
except Exception as exc2:
|
| 205 |
+
|
| 206 |
+
raise ValueError(
|
| 207 |
+
f"Model output was not valid JSON: {exc}\n\nRaw output:\n{raw_text}\n\nAttempted simple repair (trim/balance/quote):\n{repaired}\n\nRepair attempt error: {exc2}"
|
| 208 |
+
) from exc
|
| 209 |
+
|
| 210 |
+
return result
|
| 211 |
+
|
cache_manager.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cache management module for production deployment.
|
| 3 |
+
Handles caching of embeddings, search results, and regulations.
|
| 4 |
+
"""
|
| 5 |
+
from typing import Dict, Any, Optional, List
|
| 6 |
+
import hashlib
|
| 7 |
+
import json
|
| 8 |
+
from functools import lru_cache
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class CacheManager:
|
| 12 |
+
"""
|
| 13 |
+
Manages caching for improved performance.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
def __init__(self, max_size: int = 1000):
|
| 17 |
+
"""
|
| 18 |
+
Initialize cache manager.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
max_size: Maximum number of cached items
|
| 22 |
+
"""
|
| 23 |
+
self.max_size = max_size
|
| 24 |
+
self._embedding_cache: Dict[str, List[float]] = {}
|
| 25 |
+
self._search_cache: Dict[str, List[Any]] = {}
|
| 26 |
+
self._regulation_cache: Dict[str, str] = {}
|
| 27 |
+
|
| 28 |
+
def _generate_key(self, text: str) -> str:
|
| 29 |
+
"""Generate cache key from text."""
|
| 30 |
+
return hashlib.md5(text.encode()).hexdigest()
|
| 31 |
+
|
| 32 |
+
def get_embedding(self, text: str) -> Optional[List[float]]:
|
| 33 |
+
"""
|
| 34 |
+
Get cached embedding.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
text: Text to look up
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
Cached embedding or None
|
| 41 |
+
"""
|
| 42 |
+
key = self._generate_key(text)
|
| 43 |
+
return self._embedding_cache.get(key)
|
| 44 |
+
|
| 45 |
+
def set_embedding(self, text: str, embedding: List[float]):
|
| 46 |
+
"""
|
| 47 |
+
Cache an embedding.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
text: Text
|
| 51 |
+
embedding: Embedding vector
|
| 52 |
+
"""
|
| 53 |
+
if len(self._embedding_cache) >= self.max_size:
|
| 54 |
+
# Remove oldest entry (simple FIFO)
|
| 55 |
+
first_key = next(iter(self._embedding_cache))
|
| 56 |
+
del self._embedding_cache[first_key]
|
| 57 |
+
|
| 58 |
+
key = self._generate_key(text)
|
| 59 |
+
self._embedding_cache[key] = embedding
|
| 60 |
+
|
| 61 |
+
def get_search_result(self, query: str) -> Optional[List[Any]]:
|
| 62 |
+
"""
|
| 63 |
+
Get cached search result.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
query: Search query
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
Cached results or None
|
| 70 |
+
"""
|
| 71 |
+
key = self._generate_key(query)
|
| 72 |
+
return self._search_cache.get(key)
|
| 73 |
+
|
| 74 |
+
def set_search_result(self, query: str, results: List[Any]):
|
| 75 |
+
"""
|
| 76 |
+
Cache search results.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
query: Search query
|
| 80 |
+
results: Search results
|
| 81 |
+
"""
|
| 82 |
+
if len(self._search_cache) >= self.max_size:
|
| 83 |
+
first_key = next(iter(self._search_cache))
|
| 84 |
+
del self._search_cache[first_key]
|
| 85 |
+
|
| 86 |
+
key = self._generate_key(query)
|
| 87 |
+
self._search_cache[key] = results
|
| 88 |
+
|
| 89 |
+
def clear_cache(self, cache_type: Optional[str] = None):
|
| 90 |
+
"""
|
| 91 |
+
Clear cache.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
cache_type: Type of cache to clear ('embedding', 'search', or None for all)
|
| 95 |
+
"""
|
| 96 |
+
if cache_type == "embedding":
|
| 97 |
+
self._embedding_cache.clear()
|
| 98 |
+
elif cache_type == "search":
|
| 99 |
+
self._search_cache.clear()
|
| 100 |
+
elif cache_type == "regulation":
|
| 101 |
+
self._regulation_cache.clear()
|
| 102 |
+
else:
|
| 103 |
+
self._embedding_cache.clear()
|
| 104 |
+
self._search_cache.clear()
|
| 105 |
+
self._regulation_cache.clear()
|
| 106 |
+
|
| 107 |
+
def get_cache_stats(self) -> Dict[str, Any]:
|
| 108 |
+
"""
|
| 109 |
+
Get cache statistics.
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
Dictionary with cache stats
|
| 113 |
+
"""
|
| 114 |
+
return {
|
| 115 |
+
"embedding_cache_size": len(self._embedding_cache),
|
| 116 |
+
"search_cache_size": len(self._search_cache),
|
| 117 |
+
"regulation_cache_size": len(self._regulation_cache),
|
| 118 |
+
"max_size": self.max_size
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# Global instance
|
| 123 |
+
_cache_manager: Optional[CacheManager] = None
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def get_cache_manager() -> CacheManager:
|
| 127 |
+
"""Get or create global cache manager instance."""
|
| 128 |
+
global _cache_manager
|
| 129 |
+
if _cache_manager is None:
|
| 130 |
+
_cache_manager = CacheManager()
|
| 131 |
+
return _cache_manager
|
database_manager.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database management module for production deployment.
|
| 3 |
+
Handles initialization, indexing, and maintenance of the vector database.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
from typing import List, Optional, Dict, Any
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
from vector_db import get_vector_database
|
| 10 |
+
from document_processor import get_document_processor
|
| 11 |
+
from doc_utils import extract_text_from_path, is_supported_file_type
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class DatabaseManager:
|
| 15 |
+
"""
|
| 16 |
+
Manages the vector database: initialization, indexing, updates.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def __init__(self, reference_dir: str = "./reference_docs"):
|
| 20 |
+
"""
|
| 21 |
+
Initialize database manager.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
reference_dir: Directory containing reference documents
|
| 25 |
+
"""
|
| 26 |
+
self.reference_dir = reference_dir
|
| 27 |
+
self.vector_db = get_vector_database()
|
| 28 |
+
self.doc_processor = get_document_processor()
|
| 29 |
+
|
| 30 |
+
def index_reference_documents(
|
| 31 |
+
self,
|
| 32 |
+
force_reindex: bool = False,
|
| 33 |
+
file_paths: Optional[List[str]] = None
|
| 34 |
+
) -> Dict[str, Any]:
|
| 35 |
+
"""
|
| 36 |
+
Index all reference documents into the vector database.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
force_reindex: If True, clear existing data and reindex
|
| 40 |
+
file_paths: Optional list of specific files to index
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
Dictionary with indexing results
|
| 44 |
+
"""
|
| 45 |
+
results = {
|
| 46 |
+
"indexed": 0,
|
| 47 |
+
"failed": 0,
|
| 48 |
+
"errors": []
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
# Clear if force reindex
|
| 52 |
+
if force_reindex:
|
| 53 |
+
print("Clearing existing database...")
|
| 54 |
+
self.vector_db.clear_collection()
|
| 55 |
+
|
| 56 |
+
# Get files to index
|
| 57 |
+
if file_paths:
|
| 58 |
+
files_to_index = file_paths
|
| 59 |
+
else:
|
| 60 |
+
files_to_index = self._get_reference_files()
|
| 61 |
+
|
| 62 |
+
if not files_to_index:
|
| 63 |
+
print("WARNING: No files found to index.")
|
| 64 |
+
return results
|
| 65 |
+
|
| 66 |
+
print(f"Indexing {len(files_to_index)} documents...")
|
| 67 |
+
|
| 68 |
+
# Process each file
|
| 69 |
+
all_documents = []
|
| 70 |
+
|
| 71 |
+
for file_path in files_to_index:
|
| 72 |
+
try:
|
| 73 |
+
# Check file type
|
| 74 |
+
is_valid, _ = is_supported_file_type(file_path)
|
| 75 |
+
if not is_valid:
|
| 76 |
+
results["failed"] += 1
|
| 77 |
+
results["errors"].append(f"Unsupported file type: {file_path}")
|
| 78 |
+
continue
|
| 79 |
+
|
| 80 |
+
# Get label
|
| 81 |
+
label = os.path.basename(file_path)
|
| 82 |
+
|
| 83 |
+
# Process file
|
| 84 |
+
documents = self.doc_processor.process_file(file_path, source_label=label)
|
| 85 |
+
|
| 86 |
+
if documents:
|
| 87 |
+
all_documents.extend(documents)
|
| 88 |
+
results["indexed"] += 1
|
| 89 |
+
print(f" OK: Indexed: {label} ({len(documents)} chunks)")
|
| 90 |
+
else:
|
| 91 |
+
results["failed"] += 1
|
| 92 |
+
results["errors"].append(f"No content extracted: {file_path}")
|
| 93 |
+
|
| 94 |
+
except Exception as e:
|
| 95 |
+
results["failed"] += 1
|
| 96 |
+
results["errors"].append(f"Error processing {file_path}: {str(e)}")
|
| 97 |
+
print(f" FAILED: {os.path.basename(file_path)} - {e}")
|
| 98 |
+
|
| 99 |
+
# Add all documents to vector database
|
| 100 |
+
if all_documents:
|
| 101 |
+
print(f"\nAdding {len(all_documents)} document chunks to vector database...")
|
| 102 |
+
try:
|
| 103 |
+
self.vector_db.add_documents(all_documents)
|
| 104 |
+
print(f"SUCCESS: Successfully indexed {results['indexed']} documents with {len(all_documents)} chunks")
|
| 105 |
+
except Exception as e:
|
| 106 |
+
results["errors"].append(f"Failed to add documents to database: {str(e)}")
|
| 107 |
+
print(f"ERROR: Error adding documents: {e}")
|
| 108 |
+
|
| 109 |
+
return results
|
| 110 |
+
|
| 111 |
+
def index_text(
|
| 112 |
+
self,
|
| 113 |
+
text: str,
|
| 114 |
+
source_label: str,
|
| 115 |
+
metadata: Optional[Dict[str, Any]] = None
|
| 116 |
+
) -> bool:
|
| 117 |
+
"""
|
| 118 |
+
Index a text document.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
text: Text content
|
| 122 |
+
source_label: Label for the source
|
| 123 |
+
metadata: Optional metadata
|
| 124 |
+
|
| 125 |
+
Returns:
|
| 126 |
+
True if successful
|
| 127 |
+
"""
|
| 128 |
+
try:
|
| 129 |
+
documents = self.doc_processor.process_text(text, source_label, metadata)
|
| 130 |
+
if documents:
|
| 131 |
+
self.vector_db.add_documents(documents)
|
| 132 |
+
return True
|
| 133 |
+
return False
|
| 134 |
+
except Exception as e:
|
| 135 |
+
print(f"❌ Error indexing text: {e}")
|
| 136 |
+
return False
|
| 137 |
+
|
| 138 |
+
def _get_reference_files(self) -> List[str]:
|
| 139 |
+
"""
|
| 140 |
+
Get all reference files from the reference directory.
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
List of file paths
|
| 144 |
+
"""
|
| 145 |
+
files = []
|
| 146 |
+
|
| 147 |
+
if not os.path.isdir(self.reference_dir):
|
| 148 |
+
return files
|
| 149 |
+
|
| 150 |
+
for name in os.listdir(self.reference_dir):
|
| 151 |
+
full_path = os.path.join(self.reference_dir, name)
|
| 152 |
+
if os.path.isfile(full_path):
|
| 153 |
+
is_valid, _ = is_supported_file_type(full_path)
|
| 154 |
+
if is_valid:
|
| 155 |
+
files.append(full_path)
|
| 156 |
+
|
| 157 |
+
return files
|
| 158 |
+
|
| 159 |
+
def get_database_stats(self) -> Dict[str, Any]:
|
| 160 |
+
"""
|
| 161 |
+
Get statistics about the database.
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
Dictionary with statistics
|
| 165 |
+
"""
|
| 166 |
+
stats = self.vector_db.get_collection_info()
|
| 167 |
+
stats["reference_directory"] = self.reference_dir
|
| 168 |
+
stats["reference_files"] = len(self._get_reference_files())
|
| 169 |
+
return stats
|
| 170 |
+
|
| 171 |
+
def check_database_health(self) -> Dict[str, Any]:
|
| 172 |
+
"""
|
| 173 |
+
Check database health and readiness.
|
| 174 |
+
|
| 175 |
+
Returns:
|
| 176 |
+
Dictionary with health status
|
| 177 |
+
"""
|
| 178 |
+
health = {
|
| 179 |
+
"status": "healthy",
|
| 180 |
+
"database_exists": False,
|
| 181 |
+
"document_count": 0,
|
| 182 |
+
"reference_files": 0,
|
| 183 |
+
"warnings": []
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
# Check database
|
| 188 |
+
stats = self.get_database_stats()
|
| 189 |
+
health["database_exists"] = True
|
| 190 |
+
health["document_count"] = stats.get("document_count", 0)
|
| 191 |
+
health["reference_files"] = stats.get("reference_files", 0)
|
| 192 |
+
|
| 193 |
+
# Check if database is empty
|
| 194 |
+
if health["document_count"] == 0:
|
| 195 |
+
health["status"] = "empty"
|
| 196 |
+
health["warnings"].append("Database is empty. Run indexing first.")
|
| 197 |
+
|
| 198 |
+
# Check if reference files exist
|
| 199 |
+
if health["reference_files"] == 0:
|
| 200 |
+
health["warnings"].append("No reference files found in reference_docs folder.")
|
| 201 |
+
|
| 202 |
+
except Exception as e:
|
| 203 |
+
health["status"] = "error"
|
| 204 |
+
health["warnings"].append(f"Database error: {str(e)}")
|
| 205 |
+
|
| 206 |
+
return health
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# Global instance
|
| 210 |
+
_db_manager: Optional[DatabaseManager] = None
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def get_database_manager() -> DatabaseManager:
|
| 214 |
+
"""Get or create global database manager instance."""
|
| 215 |
+
global _db_manager
|
| 216 |
+
if _db_manager is None:
|
| 217 |
+
_db_manager = DatabaseManager()
|
| 218 |
+
return _db_manager
|
doc_utils.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Optional, Tuple
|
| 3 |
+
|
| 4 |
+
from docx import Document # python-docx
|
| 5 |
+
from pypdf import PdfReader
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
SUPPORTED_EXTENSIONS = {".txt", ".pdf", ".docx"}
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def is_supported_file_type(path: str) -> Tuple[bool, Optional[str]]:
|
| 12 |
+
"""
|
| 13 |
+
Check if a file path has a supported extension.
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
(is_valid, warning_message)
|
| 17 |
+
- is_valid: True if file type is supported
|
| 18 |
+
- warning_message: Warning message if not supported, None if supported
|
| 19 |
+
"""
|
| 20 |
+
_, ext = os.path.splitext(path)
|
| 21 |
+
ext = ext.lower()
|
| 22 |
+
|
| 23 |
+
if ext not in SUPPORTED_EXTENSIONS:
|
| 24 |
+
warning = (
|
| 25 |
+
f"⚠️ WARNING: Unsupported file type '{ext}' for file '{os.path.basename(path)}'. "
|
| 26 |
+
f"Only the following file types are supported: {', '.join(sorted(SUPPORTED_EXTENSIONS))}. "
|
| 27 |
+
f"This file will be skipped."
|
| 28 |
+
)
|
| 29 |
+
return False, warning
|
| 30 |
+
|
| 31 |
+
return True, None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def extract_text_from_path(path: str, show_warning: bool = True) -> str:
|
| 35 |
+
"""
|
| 36 |
+
Read text from a file path.
|
| 37 |
+
|
| 38 |
+
Supports:
|
| 39 |
+
- .txt
|
| 40 |
+
- .pdf
|
| 41 |
+
- .docx
|
| 42 |
+
|
| 43 |
+
Parameters:
|
| 44 |
+
path: File path to read
|
| 45 |
+
show_warning: If True, raises ValueError with warning for unsupported types.
|
| 46 |
+
If False, silently raises ValueError (for reference_docs folder).
|
| 47 |
+
|
| 48 |
+
Raises:
|
| 49 |
+
ValueError: If file type is not supported
|
| 50 |
+
"""
|
| 51 |
+
_, ext = os.path.splitext(path)
|
| 52 |
+
ext = ext.lower()
|
| 53 |
+
|
| 54 |
+
if ext not in SUPPORTED_EXTENSIONS:
|
| 55 |
+
if show_warning:
|
| 56 |
+
raise ValueError(
|
| 57 |
+
f"⚠️ WARNING: Unsupported file type '{ext}' for file '{os.path.basename(path)}'. "
|
| 58 |
+
f"Only the following file types are supported: {', '.join(sorted(SUPPORTED_EXTENSIONS))}. "
|
| 59 |
+
f"Please use a .txt, .pdf, or .docx file instead."
|
| 60 |
+
)
|
| 61 |
+
else:
|
| 62 |
+
raise ValueError(f"Unsupported file type: {ext}")
|
| 63 |
+
|
| 64 |
+
if ext == ".txt":
|
| 65 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 66 |
+
return f.read()
|
| 67 |
+
|
| 68 |
+
if ext == ".docx":
|
| 69 |
+
doc = Document(path)
|
| 70 |
+
return "\n".join(p.text for p in doc.paragraphs).strip()
|
| 71 |
+
|
| 72 |
+
if ext == ".pdf":
|
| 73 |
+
reader = PdfReader(path)
|
| 74 |
+
texts = []
|
| 75 |
+
for page in reader.pages:
|
| 76 |
+
page_text: Optional[str] = page.extract_text()
|
| 77 |
+
if page_text:
|
| 78 |
+
texts.append(page_text)
|
| 79 |
+
return "\n".join(texts).strip()
|
| 80 |
+
|
| 81 |
+
# Fallback (should never be reached due to ext check)
|
| 82 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 83 |
+
return f.read()
|
| 84 |
+
|
document_processor.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Document processing module for production deployment.
|
| 3 |
+
Handles text extraction, chunking, and preprocessing.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
from typing import List, Dict, Any, Optional, Tuple
|
| 7 |
+
|
| 8 |
+
# Try different import paths for langchain compatibility
|
| 9 |
+
try:
|
| 10 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 11 |
+
except ImportError:
|
| 12 |
+
try:
|
| 13 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 14 |
+
except ImportError:
|
| 15 |
+
try:
|
| 16 |
+
from langchain_text_splitter import RecursiveCharacterTextSplitter
|
| 17 |
+
except ImportError:
|
| 18 |
+
raise ImportError(
|
| 19 |
+
"langchain-text-splitters not installed. Run: pip install langchain-text-splitters"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
from langchain_core.documents import Document
|
| 24 |
+
except ImportError:
|
| 25 |
+
try:
|
| 26 |
+
from langchain_core.documents import Document
|
| 27 |
+
except ImportError:
|
| 28 |
+
raise ImportError(
|
| 29 |
+
"langchain-core not installed. Run: pip install langchain-core"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
from doc_utils import extract_text_from_path
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class DocumentProcessor:
|
| 36 |
+
"""
|
| 37 |
+
Processes documents for vector database storage.
|
| 38 |
+
Handles chunking, metadata extraction, and preprocessing.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def __init__(
|
| 42 |
+
self,
|
| 43 |
+
chunk_size: int = 1000,
|
| 44 |
+
chunk_overlap: int = 200,
|
| 45 |
+
separators: Optional[List[str]] = None
|
| 46 |
+
):
|
| 47 |
+
"""
|
| 48 |
+
Initialize document processor.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
chunk_size: Size of each chunk in characters
|
| 52 |
+
chunk_overlap: Overlap between chunks
|
| 53 |
+
separators: Text separators for splitting
|
| 54 |
+
"""
|
| 55 |
+
if separators is None:
|
| 56 |
+
separators = ["\n\n", "\n", ". ", " ", ""]
|
| 57 |
+
|
| 58 |
+
self.text_splitter = RecursiveCharacterTextSplitter(
|
| 59 |
+
chunk_size=chunk_size,
|
| 60 |
+
chunk_overlap=chunk_overlap,
|
| 61 |
+
separators=separators,
|
| 62 |
+
length_function=len,
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
def process_file(
|
| 66 |
+
self,
|
| 67 |
+
file_path: str,
|
| 68 |
+
source_label: Optional[str] = None,
|
| 69 |
+
metadata: Optional[Dict[str, Any]] = None
|
| 70 |
+
) -> List[Document]:
|
| 71 |
+
"""
|
| 72 |
+
Process a file into chunks with metadata.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
file_path: Path to the file
|
| 76 |
+
source_label: Label for the source document
|
| 77 |
+
metadata: Additional metadata to attach
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
List of Document objects with chunks and metadata
|
| 81 |
+
"""
|
| 82 |
+
# Extract text
|
| 83 |
+
try:
|
| 84 |
+
text = extract_text_from_path(file_path, show_warning=False)
|
| 85 |
+
except Exception as e:
|
| 86 |
+
print(f"⚠️ Warning: Failed to extract text from {file_path}: {e}")
|
| 87 |
+
return []
|
| 88 |
+
|
| 89 |
+
if not text or not text.strip():
|
| 90 |
+
return []
|
| 91 |
+
|
| 92 |
+
# Use filename as source if not provided
|
| 93 |
+
if source_label is None:
|
| 94 |
+
source_label = os.path.basename(file_path)
|
| 95 |
+
|
| 96 |
+
# Split into chunks
|
| 97 |
+
chunks = self.text_splitter.split_text(text)
|
| 98 |
+
|
| 99 |
+
# Create Document objects with metadata
|
| 100 |
+
documents = []
|
| 101 |
+
for idx, chunk_text in enumerate(chunks):
|
| 102 |
+
doc_metadata = {
|
| 103 |
+
"source": source_label,
|
| 104 |
+
"source_file": file_path,
|
| 105 |
+
"chunk_index": idx,
|
| 106 |
+
"total_chunks": len(chunks),
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
# Add custom metadata
|
| 110 |
+
if metadata:
|
| 111 |
+
doc_metadata.update(metadata)
|
| 112 |
+
|
| 113 |
+
documents.append(Document(
|
| 114 |
+
page_content=chunk_text,
|
| 115 |
+
metadata=doc_metadata
|
| 116 |
+
))
|
| 117 |
+
|
| 118 |
+
return documents
|
| 119 |
+
|
| 120 |
+
def process_text(
|
| 121 |
+
self,
|
| 122 |
+
text: str,
|
| 123 |
+
source_label: str,
|
| 124 |
+
metadata: Optional[Dict[str, Any]] = None
|
| 125 |
+
) -> List[Document]:
|
| 126 |
+
"""
|
| 127 |
+
Process raw text into chunks with metadata.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
text: Raw text to process
|
| 131 |
+
source_label: Label for the source
|
| 132 |
+
metadata: Additional metadata
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
List of Document objects
|
| 136 |
+
"""
|
| 137 |
+
if not text or not text.strip():
|
| 138 |
+
return []
|
| 139 |
+
|
| 140 |
+
# Split into chunks
|
| 141 |
+
chunks = self.text_splitter.split_text(text)
|
| 142 |
+
|
| 143 |
+
# Create Document objects
|
| 144 |
+
documents = []
|
| 145 |
+
for idx, chunk_text in enumerate(chunks):
|
| 146 |
+
doc_metadata = {
|
| 147 |
+
"source": source_label,
|
| 148 |
+
"chunk_index": idx,
|
| 149 |
+
"total_chunks": len(chunks),
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
if metadata:
|
| 153 |
+
doc_metadata.update(metadata)
|
| 154 |
+
|
| 155 |
+
documents.append(Document(
|
| 156 |
+
page_content=chunk_text,
|
| 157 |
+
metadata=doc_metadata
|
| 158 |
+
))
|
| 159 |
+
|
| 160 |
+
return documents
|
| 161 |
+
|
| 162 |
+
def process_multiple_files(
|
| 163 |
+
self,
|
| 164 |
+
file_paths: List[str],
|
| 165 |
+
source_labels: Optional[List[str]] = None
|
| 166 |
+
) -> List[Document]:
|
| 167 |
+
"""
|
| 168 |
+
Process multiple files.
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
file_paths: List of file paths
|
| 172 |
+
source_labels: Optional list of labels (one per file)
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
Combined list of all Document objects
|
| 176 |
+
"""
|
| 177 |
+
all_documents = []
|
| 178 |
+
|
| 179 |
+
for idx, file_path in enumerate(file_paths):
|
| 180 |
+
label = source_labels[idx] if source_labels and idx < len(source_labels) else None
|
| 181 |
+
documents = self.process_file(file_path, source_label=label)
|
| 182 |
+
all_documents.extend(documents)
|
| 183 |
+
|
| 184 |
+
return all_documents
|
| 185 |
+
|
| 186 |
+
def preprocess_text(self, text: str) -> str:
|
| 187 |
+
"""
|
| 188 |
+
Preprocess text (clean, normalize).
|
| 189 |
+
|
| 190 |
+
Args:
|
| 191 |
+
text: Raw text
|
| 192 |
+
|
| 193 |
+
Returns:
|
| 194 |
+
Cleaned text
|
| 195 |
+
"""
|
| 196 |
+
# Remove excessive whitespace
|
| 197 |
+
text = " ".join(text.split())
|
| 198 |
+
|
| 199 |
+
# Remove special characters that might interfere
|
| 200 |
+
# (Keep basic punctuation)
|
| 201 |
+
|
| 202 |
+
return text.strip()
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
# Global instance
|
| 206 |
+
_document_processor: Optional[DocumentProcessor] = None
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def get_document_processor() -> DocumentProcessor:
|
| 210 |
+
"""Get or create global document processor instance."""
|
| 211 |
+
global _document_processor
|
| 212 |
+
if _document_processor is None:
|
| 213 |
+
_document_processor = DocumentProcessor()
|
| 214 |
+
return _document_processor
|
embeddings.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Embedding generation module for production deployment.
|
| 3 |
+
Uses Google's embedding models to convert text to vectors.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
from typing import List, Optional
|
| 7 |
+
from functools import lru_cache
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
|
| 10 |
+
load_dotenv(override=True)
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
from langchain_google_genai import GoogleGenerativeAIEmbeddings
|
| 14 |
+
except ImportError:
|
| 15 |
+
GoogleGenerativeAIEmbeddings = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class EmbeddingGenerator:
|
| 19 |
+
"""
|
| 20 |
+
Handles embedding generation with caching for production efficiency.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, model_name: str = "models/embedding-001"):
|
| 24 |
+
"""
|
| 25 |
+
Initialize embedding generator.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
model_name: Google embedding model name
|
| 29 |
+
"""
|
| 30 |
+
self.model_name = model_name
|
| 31 |
+
self.api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
|
| 32 |
+
|
| 33 |
+
if not self.api_key:
|
| 34 |
+
raise RuntimeError("GEMINI_API_KEY not found. Set it in .env file.")
|
| 35 |
+
|
| 36 |
+
if GoogleGenerativeAIEmbeddings is None:
|
| 37 |
+
raise RuntimeError(
|
| 38 |
+
"langchain-google-genai not installed. Run: pip install langchain-google-genai"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Initialize embeddings model
|
| 42 |
+
model_id = model_name.replace("models/", "")
|
| 43 |
+
self.embeddings = GoogleGenerativeAIEmbeddings(
|
| 44 |
+
model=model_id,
|
| 45 |
+
google_api_key=self.api_key
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Cache for embeddings (in-memory)
|
| 49 |
+
self._embedding_cache = {}
|
| 50 |
+
|
| 51 |
+
def generate_embedding(self, text: str, use_cache: bool = True) -> List[float]:
|
| 52 |
+
"""
|
| 53 |
+
Generate embedding for a single text.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
text: Text to embed
|
| 57 |
+
use_cache: Whether to use cached embeddings
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
Embedding vector
|
| 61 |
+
"""
|
| 62 |
+
if not text or not text.strip():
|
| 63 |
+
# Return zero vector for empty text
|
| 64 |
+
return [0.0] * 768 # Default embedding dimension
|
| 65 |
+
|
| 66 |
+
# Check cache
|
| 67 |
+
if use_cache and text in self._embedding_cache:
|
| 68 |
+
return self._embedding_cache[text]
|
| 69 |
+
|
| 70 |
+
try:
|
| 71 |
+
# Generate embedding
|
| 72 |
+
embedding = self.embeddings.embed_query(text)
|
| 73 |
+
|
| 74 |
+
# Cache it
|
| 75 |
+
if use_cache:
|
| 76 |
+
self._embedding_cache[text] = embedding
|
| 77 |
+
|
| 78 |
+
return embedding
|
| 79 |
+
except Exception as e:
|
| 80 |
+
print(f"⚠️ Warning: Embedding generation failed: {e}")
|
| 81 |
+
# Return zero vector on error
|
| 82 |
+
return [0.0] * 768
|
| 83 |
+
|
| 84 |
+
def generate_embeddings_batch(
|
| 85 |
+
self,
|
| 86 |
+
texts: List[str],
|
| 87 |
+
use_cache: bool = True,
|
| 88 |
+
batch_size: int = 100
|
| 89 |
+
) -> List[List[float]]:
|
| 90 |
+
"""
|
| 91 |
+
Generate embeddings for multiple texts efficiently.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
texts: List of texts to embed
|
| 95 |
+
use_cache: Whether to use cached embeddings
|
| 96 |
+
batch_size: Number of texts to process at once
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
List of embedding vectors
|
| 100 |
+
"""
|
| 101 |
+
results = []
|
| 102 |
+
|
| 103 |
+
for i in range(0, len(texts), batch_size):
|
| 104 |
+
batch = texts[i:i + batch_size]
|
| 105 |
+
|
| 106 |
+
# Check cache for batch
|
| 107 |
+
cached = []
|
| 108 |
+
uncached = []
|
| 109 |
+
uncached_indices = []
|
| 110 |
+
|
| 111 |
+
for idx, text in enumerate(batch):
|
| 112 |
+
if use_cache and text in self._embedding_cache:
|
| 113 |
+
cached.append(self._embedding_cache[text])
|
| 114 |
+
else:
|
| 115 |
+
uncached.append(text)
|
| 116 |
+
uncached_indices.append(idx)
|
| 117 |
+
|
| 118 |
+
# Generate embeddings for uncached texts
|
| 119 |
+
if uncached:
|
| 120 |
+
try:
|
| 121 |
+
batch_embeddings = self.embeddings.embed_documents(uncached)
|
| 122 |
+
|
| 123 |
+
# Cache them
|
| 124 |
+
if use_cache:
|
| 125 |
+
for text, embedding in zip(uncached, batch_embeddings):
|
| 126 |
+
self._embedding_cache[text] = embedding
|
| 127 |
+
except Exception as e:
|
| 128 |
+
print(f"⚠️ Warning: Batch embedding failed: {e}")
|
| 129 |
+
# Use zero vectors for failed embeddings
|
| 130 |
+
batch_embeddings = [[0.0] * 768] * len(uncached)
|
| 131 |
+
else:
|
| 132 |
+
batch_embeddings = []
|
| 133 |
+
|
| 134 |
+
# Reconstruct batch results
|
| 135 |
+
batch_results = [None] * len(batch)
|
| 136 |
+
cache_idx = 0
|
| 137 |
+
embed_idx = 0
|
| 138 |
+
|
| 139 |
+
for idx in range(len(batch)):
|
| 140 |
+
if idx in uncached_indices:
|
| 141 |
+
batch_results[idx] = batch_embeddings[embed_idx]
|
| 142 |
+
embed_idx += 1
|
| 143 |
+
else:
|
| 144 |
+
batch_results[idx] = cached[cache_idx]
|
| 145 |
+
cache_idx += 1
|
| 146 |
+
|
| 147 |
+
results.extend(batch_results)
|
| 148 |
+
|
| 149 |
+
return results
|
| 150 |
+
|
| 151 |
+
def clear_cache(self):
|
| 152 |
+
"""Clear the embedding cache."""
|
| 153 |
+
self._embedding_cache.clear()
|
| 154 |
+
|
| 155 |
+
def get_cache_size(self) -> int:
|
| 156 |
+
"""Get the number of cached embeddings."""
|
| 157 |
+
return len(self._embedding_cache)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# Global instance
|
| 161 |
+
_embedding_generator: Optional[EmbeddingGenerator] = None
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def get_embedding_generator() -> EmbeddingGenerator:
|
| 165 |
+
"""Get or create global embedding generator instance."""
|
| 166 |
+
global _embedding_generator
|
| 167 |
+
if _embedding_generator is None:
|
| 168 |
+
_embedding_generator = EmbeddingGenerator()
|
| 169 |
+
return _embedding_generator
|
gradio_app.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from typing import List, Tuple
|
| 4 |
+
|
| 5 |
+
import gradio as gr
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
from auditor import call_llm_with_gemini
|
| 9 |
+
from doc_utils import extract_text_from_path, is_supported_file_type
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
REFERENCE_DIR = os.path.join(os.path.dirname(__file__), "reference_docs")
|
| 13 |
+
DEFAULT_GEMINI_MODEL = os.getenv("GEMINI_MODEL", "models/gemini-2.5-pro")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def configure_gemini_model():
|
| 17 |
+
"""
|
| 18 |
+
Configure and return a Gemini model instance using GEMINI_API_KEY.
|
| 19 |
+
"""
|
| 20 |
+
# override=True ensures new keys in .env replace any old environment values
|
| 21 |
+
load_dotenv(override=True)
|
| 22 |
+
api_key = os.getenv("GEMINI_API_KEY")
|
| 23 |
+
if not api_key:
|
| 24 |
+
raise RuntimeError("GEMINI_API_KEY not found. Please set it in a .env file or environment variable.")
|
| 25 |
+
|
| 26 |
+
# If GOOGLE_API_KEY is set in the environment, google-genai may prefer it.
|
| 27 |
+
# We explicitly remove it so GEMINI_API_KEY is always used.
|
| 28 |
+
os.environ.pop("GOOGLE_API_KEY", None)
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
from google import genai
|
| 32 |
+
except Exception as exc:
|
| 33 |
+
raise RuntimeError("google-genai not installed. Run: pip install -r requirements.txt") from exc
|
| 34 |
+
|
| 35 |
+
client = genai.Client(api_key=api_key)
|
| 36 |
+
return client
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def run_audit(
|
| 40 |
+
user_document_text: str,
|
| 41 |
+
user_document_file: str,
|
| 42 |
+
rag_text: str,
|
| 43 |
+
rag_files: List[str],
|
| 44 |
+
) -> Tuple[str, str]:
|
| 45 |
+
"""
|
| 46 |
+
Gradio callback to run the compliance audit.
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
- JSON string (pretty-printed)
|
| 50 |
+
- Markdown report
|
| 51 |
+
"""
|
| 52 |
+
# Resolve main user document text:
|
| 53 |
+
# If a file is uploaded, it overrides the textbox content.
|
| 54 |
+
user_document_text = (user_document_text or "").strip()
|
| 55 |
+
user_document_file = (user_document_file or "").strip()
|
| 56 |
+
|
| 57 |
+
warnings = []
|
| 58 |
+
|
| 59 |
+
if user_document_file:
|
| 60 |
+
# Check file type for user-uploaded document
|
| 61 |
+
is_valid, warning_msg = is_supported_file_type(user_document_file)
|
| 62 |
+
if not is_valid:
|
| 63 |
+
warnings.append(warning_msg)
|
| 64 |
+
return "\n".join(warnings) + "\n\nERROR: Please upload a supported file type (.txt, .pdf, or .docx) or paste text instead.", ""
|
| 65 |
+
|
| 66 |
+
try:
|
| 67 |
+
user_document = extract_text_from_path(user_document_file, show_warning=True)
|
| 68 |
+
except Exception as exc:
|
| 69 |
+
return f"ERROR reading uploaded user document: {exc}", ""
|
| 70 |
+
else:
|
| 71 |
+
user_document = user_document_text
|
| 72 |
+
|
| 73 |
+
if not user_document.strip():
|
| 74 |
+
return "ERROR: Please provide a user document to be audited.", ""
|
| 75 |
+
|
| 76 |
+
rag_contexts: List[Tuple[str, str]] = []
|
| 77 |
+
|
| 78 |
+
# Check whether user has provided any reference documents (text or files).
|
| 79 |
+
user_has_refs = bool(rag_text and rag_text.strip()) or bool(rag_files)
|
| 80 |
+
|
| 81 |
+
if user_has_refs:
|
| 82 |
+
# Use ONLY the references explicitly provided by the user.
|
| 83 |
+
if rag_text and rag_text.strip():
|
| 84 |
+
rag_contexts.append(("Pasted RAG Text", rag_text.strip()))
|
| 85 |
+
|
| 86 |
+
if rag_files:
|
| 87 |
+
for path in rag_files:
|
| 88 |
+
# Check file type for user-uploaded reference files
|
| 89 |
+
is_valid, warning_msg = is_supported_file_type(path)
|
| 90 |
+
if not is_valid:
|
| 91 |
+
warnings.append(warning_msg)
|
| 92 |
+
continue # Skip this file
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
label = os.path.basename(path)
|
| 96 |
+
content = extract_text_from_path(path, show_warning=True)
|
| 97 |
+
rag_contexts.append((label, content))
|
| 98 |
+
except Exception as exc:
|
| 99 |
+
warnings.append(f"⚠️ WARNING: Error reading file '{os.path.basename(path)}': {exc}")
|
| 100 |
+
else:
|
| 101 |
+
# No user-provided reference docs: fall back to reference_docs directory.
|
| 102 |
+
# Silently skip unsupported files in reference_docs (no warnings).
|
| 103 |
+
if os.path.isdir(REFERENCE_DIR):
|
| 104 |
+
for name in os.listdir(REFERENCE_DIR):
|
| 105 |
+
full_path = os.path.join(REFERENCE_DIR, name)
|
| 106 |
+
if not os.path.isfile(full_path):
|
| 107 |
+
continue
|
| 108 |
+
try:
|
| 109 |
+
# Silently skip unsupported files in reference_docs folder
|
| 110 |
+
content = extract_text_from_path(full_path, show_warning=False)
|
| 111 |
+
except Exception:
|
| 112 |
+
continue # Silently skip unsupported or unreadable files
|
| 113 |
+
rag_contexts.append((name, content))
|
| 114 |
+
|
| 115 |
+
# Show warnings if any (before running audit)
|
| 116 |
+
warning_text = ""
|
| 117 |
+
if warnings:
|
| 118 |
+
warning_text = "\n\n".join(warnings) + "\n\n" + "="*50 + "\n\n"
|
| 119 |
+
|
| 120 |
+
# Check database health (silently)
|
| 121 |
+
db_status = ""
|
| 122 |
+
try:
|
| 123 |
+
from database_manager import get_database_manager
|
| 124 |
+
db_manager = get_database_manager()
|
| 125 |
+
health = db_manager.check_database_health()
|
| 126 |
+
if health["status"] == "healthy" and health["document_count"] > 0:
|
| 127 |
+
db_status = f"Vector DB: {health['document_count']} chunks indexed | "
|
| 128 |
+
elif health["status"] == "empty":
|
| 129 |
+
db_status = "Vector DB empty - using provided documents | "
|
| 130 |
+
except:
|
| 131 |
+
pass
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
client = configure_gemini_model()
|
| 135 |
+
model_name = os.getenv("GEMINI_MODEL", DEFAULT_GEMINI_MODEL)
|
| 136 |
+
use_agentic = os.getenv("USE_AGENTIC", "true").lower() == "true"
|
| 137 |
+
result = call_llm_with_gemini(client, model_name, user_document, rag_contexts, use_agentic=use_agentic)
|
| 138 |
+
except Exception as exc:
|
| 139 |
+
model_name = os.getenv("GEMINI_MODEL", DEFAULT_GEMINI_MODEL)
|
| 140 |
+
msg = f"ERROR while calling Gemini model '{model_name}': {exc}"
|
| 141 |
+
|
| 142 |
+
if "NOT_FOUND" in str(exc) or "404" in str(exc) or "not found" in str(exc).lower():
|
| 143 |
+
msg += (
|
| 144 |
+
"\n\n❌ **Model not found!** The model name you specified doesn't exist."
|
| 145 |
+
"\n\n**To see available models, run:**"
|
| 146 |
+
"\n```bash"
|
| 147 |
+
"\npython list_gemini_models.py"
|
| 148 |
+
"\n```"
|
| 149 |
+
"\n\n**Recommended models:**"
|
| 150 |
+
"\n- `models/gemini-2.5-pro` (stable, best quality)"
|
| 151 |
+
"\n- `models/gemini-2.5-flash` (faster, cheaper)"
|
| 152 |
+
"\n- `models/gemini-3-pro-preview` (latest preview)"
|
| 153 |
+
"\n\nSet `GEMINI_MODEL` in your `.env` file to one of the available models."
|
| 154 |
+
)
|
| 155 |
+
elif "RESOURCE_EXHAUSTED" in str(exc) or "429" in str(exc) or "quota" in str(exc).lower():
|
| 156 |
+
msg += (
|
| 157 |
+
"\n\nYour Gemini API key currently has **no available quota** for this model."
|
| 158 |
+
"\n- If you intended to use a paid plan, enable billing for the Gemini API project."
|
| 159 |
+
"\n- Or switch to a different available model in your `.env` via `GEMINI_MODEL=`."
|
| 160 |
+
"\n\n**Tip:** Run `python list_gemini_models.py` to see available models."
|
| 161 |
+
)
|
| 162 |
+
else:
|
| 163 |
+
msg += (
|
| 164 |
+
"\n\n**Tip:** Run `python list_gemini_models.py` to see available models."
|
| 165 |
+
)
|
| 166 |
+
return warning_text + msg, ""
|
| 167 |
+
|
| 168 |
+
json_str = json.dumps(result, indent=2, ensure_ascii=False)
|
| 169 |
+
human_md = result.get("humanized_summary_markdown", "")
|
| 170 |
+
|
| 171 |
+
# Prepend warnings to JSON output if any
|
| 172 |
+
if warnings:
|
| 173 |
+
json_str = warning_text + json_str
|
| 174 |
+
|
| 175 |
+
return json_str, human_md
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def build_interface() -> gr.Blocks:
|
| 179 |
+
with gr.Blocks(title="AI Legal Compliance Auditor & Redrafter") as demo:
|
| 180 |
+
gr.Markdown(
|
| 181 |
+
"""
|
| 182 |
+
## AI Legal Compliance Auditor & Redrafter
|
| 183 |
+
|
| 184 |
+
**Features:**
|
| 185 |
+
- **Vector Database**: Semantic search for regulations
|
| 186 |
+
- **Agentic AI**: Multi-step reasoning with tools
|
| 187 |
+
- **Hybrid Search**: Semantic + keyword search
|
| 188 |
+
|
| 189 |
+
**Supported file types:** `.txt`, `.pdf`, `.docx` only
|
| 190 |
+
|
| 191 |
+
- Upload or paste the **document to be audited** (Word, PDF, or text file).
|
| 192 |
+
- Optionally paste additional **Acts / policies / regulations** in the text box.
|
| 193 |
+
- Or upload one or more **reference documents** (.txt, .pdf, or .docx files).
|
| 194 |
+
- If you don't provide reference documents, the system will use **semantic search** from the vector database.
|
| 195 |
+
- Click **Run audit** to get:
|
| 196 |
+
- A structured **JSON compliance audit**, and
|
| 197 |
+
- A **Markdown Compliance Audit Report** ready to share.
|
| 198 |
+
|
| 199 |
+
"""
|
| 200 |
+
)
|
| 201 |
+
##**💡 Tip**: Run `python initialize_database.py` to index reference documents for semantic search.
|
| 202 |
+
|
| 203 |
+
with gr.Row():
|
| 204 |
+
user_document_file = gr.File(
|
| 205 |
+
label="Upload user document (.txt, .pdf, .docx) - optional",
|
| 206 |
+
file_count="single",
|
| 207 |
+
type="filepath",
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
with gr.Row():
|
| 211 |
+
user_document = gr.Textbox(
|
| 212 |
+
label="User document to be audited (paste text if no file uploaded)",
|
| 213 |
+
placeholder="Paste the full text of the contract / policy / handbook here...",
|
| 214 |
+
lines=20,
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
with gr.Row():
|
| 218 |
+
rag_text = gr.Textbox(
|
| 219 |
+
label="RAG context (Acts / Policies / Regulations) - optional",
|
| 220 |
+
placeholder="Paste any legal / regulatory excerpts here...",
|
| 221 |
+
lines=12,
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
with gr.Row():
|
| 225 |
+
rag_files = gr.Files(
|
| 226 |
+
label="Reference documents (optional, .txt / .pdf / .docx)",
|
| 227 |
+
file_count="multiple",
|
| 228 |
+
type="filepath",
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
run_button = gr.Button("Run audit", variant="primary")
|
| 232 |
+
|
| 233 |
+
with gr.Row():
|
| 234 |
+
json_output = gr.Textbox(
|
| 235 |
+
label="JSON audit result",
|
| 236 |
+
lines=20,
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
md_output = gr.Markdown(label="Compliance Audit Report (Markdown)")
|
| 240 |
+
|
| 241 |
+
run_button.click(
|
| 242 |
+
fn=run_audit,
|
| 243 |
+
inputs=[user_document, user_document_file, rag_text, rag_files],
|
| 244 |
+
outputs=[json_output, md_output],
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
return demo
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
if __name__ == "__main__":
|
| 251 |
+
app = build_interface()
|
| 252 |
+
app.launch()
|
| 253 |
+
|
initialize_database.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database initialization script for production deployment.
|
| 3 |
+
Run this script to index reference documents into the vector database.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
from database_manager import get_database_manager
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def main():
|
| 13 |
+
"""Initialize the vector database with reference documents."""
|
| 14 |
+
print("=" * 60)
|
| 15 |
+
print("Vector Database Initialization")
|
| 16 |
+
print("=" * 60)
|
| 17 |
+
print()
|
| 18 |
+
|
| 19 |
+
# Check if reference_docs directory exists
|
| 20 |
+
reference_dir = "./reference_docs"
|
| 21 |
+
if not os.path.isdir(reference_dir):
|
| 22 |
+
print(f"ERROR: Reference directory '{reference_dir}' not found.")
|
| 23 |
+
print(f" Please create the directory and add regulation documents.")
|
| 24 |
+
sys.exit(1)
|
| 25 |
+
|
| 26 |
+
# Get database manager
|
| 27 |
+
try:
|
| 28 |
+
db_manager = get_database_manager()
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"ERROR: Error initializing database manager: {e}")
|
| 31 |
+
print(" Make sure all dependencies are installed:")
|
| 32 |
+
print(" pip install -r requirements.txt")
|
| 33 |
+
sys.exit(1)
|
| 34 |
+
|
| 35 |
+
# Check database health
|
| 36 |
+
print("Checking database health...")
|
| 37 |
+
health = db_manager.check_database_health()
|
| 38 |
+
|
| 39 |
+
if health["status"] == "healthy" and health["document_count"] > 0:
|
| 40 |
+
print(f"OK: Database is healthy with {health['document_count']} documents.")
|
| 41 |
+
response = input("\nDo you want to reindex? (y/n): ").strip().lower()
|
| 42 |
+
if response != 'y':
|
| 43 |
+
print("Skipping reindexing.")
|
| 44 |
+
sys.exit(0)
|
| 45 |
+
force_reindex = True
|
| 46 |
+
else:
|
| 47 |
+
print("WARNING: Database is empty or has issues.")
|
| 48 |
+
force_reindex = False
|
| 49 |
+
|
| 50 |
+
# Index documents
|
| 51 |
+
print("\nStarting indexing process...")
|
| 52 |
+
results = db_manager.index_reference_documents(force_reindex=force_reindex)
|
| 53 |
+
|
| 54 |
+
# Print results
|
| 55 |
+
print("\n" + "=" * 60)
|
| 56 |
+
print("Indexing Results")
|
| 57 |
+
print("=" * 60)
|
| 58 |
+
print(f"SUCCESS: Successfully indexed: {results['indexed']} documents")
|
| 59 |
+
print(f"FAILED: {results['failed']} documents")
|
| 60 |
+
|
| 61 |
+
if results['errors']:
|
| 62 |
+
print(f"\nWARNINGS: Errors ({len(results['errors'])}):")
|
| 63 |
+
for error in results['errors'][:10]: # Show first 10 errors
|
| 64 |
+
print(f" - {error}")
|
| 65 |
+
if len(results['errors']) > 10:
|
| 66 |
+
print(f" ... and {len(results['errors']) - 10} more errors")
|
| 67 |
+
|
| 68 |
+
# Show database stats
|
| 69 |
+
print("\n" + "=" * 60)
|
| 70 |
+
print("Database Statistics")
|
| 71 |
+
print("=" * 60)
|
| 72 |
+
stats = db_manager.get_database_stats()
|
| 73 |
+
print(f"Collection: {stats.get('collection_name', 'N/A')}")
|
| 74 |
+
print(f"Document chunks: {stats.get('document_count', 0)}")
|
| 75 |
+
print(f"Reference files: {stats.get('reference_files', 0)}")
|
| 76 |
+
|
| 77 |
+
print("\nSUCCESS: Database initialization complete!")
|
| 78 |
+
print("\nYou can now use the compliance auditor with semantic search.")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
if __name__ == "__main__":
|
| 82 |
+
main()
|
list_gemini_models.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility script to list available Gemini models.
|
| 3 |
+
Run this to see which models you can use with your API key.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
load_dotenv(override=True)
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
from google import genai
|
| 12 |
+
|
| 13 |
+
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
|
| 14 |
+
if not api_key:
|
| 15 |
+
print("ERROR: GEMINI_API_KEY or GOOGLE_API_KEY not found in .env file or environment.")
|
| 16 |
+
exit(1)
|
| 17 |
+
|
| 18 |
+
client = genai.Client(api_key=api_key)
|
| 19 |
+
models = list(client.models.list())
|
| 20 |
+
|
| 21 |
+
import sys
|
| 22 |
+
# Fix encoding for Windows console
|
| 23 |
+
if sys.platform == "win32":
|
| 24 |
+
import codecs
|
| 25 |
+
sys.stdout = codecs.getwriter("utf-8")(sys.stdout.buffer, "strict")
|
| 26 |
+
sys.stderr = codecs.getwriter("utf-8")(sys.stderr.buffer, "strict")
|
| 27 |
+
|
| 28 |
+
print(f"\n=== Available Gemini Models ({len(models)} total) ===\n")
|
| 29 |
+
|
| 30 |
+
# Filter and categorize models
|
| 31 |
+
text_models = [m for m in models if "gemini" in m.name.lower() and "generate" not in m.name.lower() and "embedding" not in m.name.lower()]
|
| 32 |
+
embedding_models = [m for m in models if "embedding" in m.name.lower()]
|
| 33 |
+
other_models = [m for m in models if m not in text_models and m not in embedding_models]
|
| 34 |
+
|
| 35 |
+
print("TEXT GENERATION MODELS (for compliance auditing):")
|
| 36 |
+
print("-" * 60)
|
| 37 |
+
for model in sorted(text_models, key=lambda x: x.name):
|
| 38 |
+
# Highlight recommended models
|
| 39 |
+
if "gemini-2.5-pro" in model.name and "preview" not in model.name:
|
| 40 |
+
print(f" [RECOMMENDED] {model.name}")
|
| 41 |
+
elif "gemini-2.5-flash" in model.name and "preview" not in model.name:
|
| 42 |
+
print(f" [FAST] {model.name}")
|
| 43 |
+
elif "gemini-3-pro" in model.name:
|
| 44 |
+
print(f" [PREVIEW] {model.name}")
|
| 45 |
+
else:
|
| 46 |
+
print(f" - {model.name}")
|
| 47 |
+
|
| 48 |
+
if embedding_models:
|
| 49 |
+
print(f"\nEMBEDDING MODELS ({len(embedding_models)}):")
|
| 50 |
+
print("-" * 60)
|
| 51 |
+
for model in sorted(embedding_models, key=lambda x: x.name)[:5]:
|
| 52 |
+
print(f" - {model.name}")
|
| 53 |
+
if len(embedding_models) > 5:
|
| 54 |
+
print(f" ... and {len(embedding_models) - 5} more")
|
| 55 |
+
|
| 56 |
+
print(f"\nTIP: Set GEMINI_MODEL in your .env file to one of the models above.")
|
| 57 |
+
print(f" Example: GEMINI_MODEL=models/gemini-2.5-pro\n")
|
| 58 |
+
|
| 59 |
+
except ImportError:
|
| 60 |
+
print("ERROR: google-genai not installed. Run: pip install -r requirements.txt")
|
| 61 |
+
except Exception as exc:
|
| 62 |
+
print(f"ERROR: {exc}")
|
main.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import List, Tuple
|
| 3 |
+
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
from auditor import call_llm_with_gemini
|
| 7 |
+
from doc_utils import extract_text_from_path, is_supported_file_type
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
REFERENCE_DIR = os.path.join(os.path.dirname(__file__), "reference_docs")
|
| 11 |
+
DEFAULT_GEMINI_MODEL = os.getenv("GEMINI_MODEL", "models/gemini-2.5-flash")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def prompt_for_document(prompt_label: str) -> str:
|
| 15 |
+
print(f"\n=== {prompt_label} ===")
|
| 16 |
+
print("Choose input method:")
|
| 17 |
+
print("1) Paste text directly")
|
| 18 |
+
print("2) Provide path to a file (.txt, .pdf, .docx)")
|
| 19 |
+
choice = input("Enter 1 or 2: ").strip()
|
| 20 |
+
|
| 21 |
+
if choice == "1":
|
| 22 |
+
print("\nPaste your text below. End with a single line containing only 'EOF':\n")
|
| 23 |
+
lines: List[str] = []
|
| 24 |
+
while True:
|
| 25 |
+
line = input()
|
| 26 |
+
if line.strip() == "EOF":
|
| 27 |
+
break
|
| 28 |
+
lines.append(line)
|
| 29 |
+
return "\n".join(lines).strip()
|
| 30 |
+
elif choice == "2":
|
| 31 |
+
path = input("Enter file path: ").strip().strip('"')
|
| 32 |
+
# Check file type for user-provided document
|
| 33 |
+
is_valid, warning_msg = is_supported_file_type(path)
|
| 34 |
+
if not is_valid:
|
| 35 |
+
print(f"\n{warning_msg}")
|
| 36 |
+
print("Please try again with a supported file type (.txt, .pdf, or .docx).\n")
|
| 37 |
+
return prompt_for_document(prompt_label)
|
| 38 |
+
return extract_text_from_path(path, show_warning=True)
|
| 39 |
+
else:
|
| 40 |
+
print("Invalid choice, please try again.")
|
| 41 |
+
return prompt_for_document(prompt_label)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def prompt_for_rag_contexts() -> List[Tuple[str, str]]:
|
| 45 |
+
print("\n=== RAG CONTEXT DOCUMENTS (Acts / Policies / Regulations) ===")
|
| 46 |
+
contexts: List[Tuple[str, str]] = []
|
| 47 |
+
|
| 48 |
+
while True:
|
| 49 |
+
print("\nAdd a new RAG context?")
|
| 50 |
+
print("1) Yes - paste text")
|
| 51 |
+
print("2) Yes - from file (.txt, .pdf, .docx)")
|
| 52 |
+
print("3) No more RAG documents (continue)")
|
| 53 |
+
choice = input("Enter 1, 2, or 3: ").strip()
|
| 54 |
+
|
| 55 |
+
if choice == "3":
|
| 56 |
+
break
|
| 57 |
+
|
| 58 |
+
label = input("Enter a short label for this RAG source (e.g., 'IT Act Sec 43A'): ").strip()
|
| 59 |
+
|
| 60 |
+
if choice == "1":
|
| 61 |
+
print("\nPaste the RAG context text. End with a single line containing only 'EOF':\n")
|
| 62 |
+
lines: List[str] = []
|
| 63 |
+
while True:
|
| 64 |
+
line = input()
|
| 65 |
+
if line.strip() == "EOF":
|
| 66 |
+
break
|
| 67 |
+
lines.append(line)
|
| 68 |
+
contexts.append((label, "\n".join(lines).strip()))
|
| 69 |
+
elif choice == "2":
|
| 70 |
+
path = input("Enter file path: ").strip().strip('"')
|
| 71 |
+
# Check file type for user-provided reference file
|
| 72 |
+
is_valid, warning_msg = is_supported_file_type(path)
|
| 73 |
+
if not is_valid:
|
| 74 |
+
print(f"\n{warning_msg}")
|
| 75 |
+
print("Skipping this file. Please try again with a supported file type (.txt, .pdf, or .docx).\n")
|
| 76 |
+
continue
|
| 77 |
+
try:
|
| 78 |
+
contexts.append((label, extract_text_from_path(path, show_warning=True)))
|
| 79 |
+
except Exception as exc:
|
| 80 |
+
print(f" WARNING: Error reading file '{path}': {exc}")
|
| 81 |
+
print("Skipping this file.\n")
|
| 82 |
+
else:
|
| 83 |
+
print("Invalid choice. Please try again.")
|
| 84 |
+
|
| 85 |
+
# If user did not provide any RAG contexts, fall back to reference_docs folder.
|
| 86 |
+
# Silently skip unsupported files in reference_docs (no warnings).
|
| 87 |
+
if not contexts:
|
| 88 |
+
if os.path.isdir(REFERENCE_DIR):
|
| 89 |
+
for name in os.listdir(REFERENCE_DIR):
|
| 90 |
+
full_path = os.path.join(REFERENCE_DIR, name)
|
| 91 |
+
if not os.path.isfile(full_path):
|
| 92 |
+
continue
|
| 93 |
+
try:
|
| 94 |
+
# Silently skip unsupported files in reference_docs folder
|
| 95 |
+
text = extract_text_from_path(full_path, show_warning=False)
|
| 96 |
+
except Exception:
|
| 97 |
+
continue # Silently skip unsupported or unreadable files
|
| 98 |
+
contexts.append((name, text))
|
| 99 |
+
|
| 100 |
+
return contexts
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def configure_gemini_model():
|
| 104 |
+
# override=True ensures new keys in .env replace any old environment values
|
| 105 |
+
load_dotenv(override=True)
|
| 106 |
+
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
|
| 107 |
+
if not api_key:
|
| 108 |
+
raise RuntimeError("GEMINI_API_KEY not found. Please set it in a .env file or environment variable.")
|
| 109 |
+
|
| 110 |
+
# If GOOGLE_API_KEY is set in the environment, google-genai may prefer it.
|
| 111 |
+
# We explicitly remove it so GEMINI_API_KEY is always used.
|
| 112 |
+
os.environ.pop("GOOGLE_API_KEY", None)
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
from google import genai
|
| 116 |
+
except Exception as exc:
|
| 117 |
+
raise RuntimeError("google-genai not installed. Run: pip install -r requirements.txt") from exc
|
| 118 |
+
|
| 119 |
+
client = genai.Client(api_key=api_key)
|
| 120 |
+
return client
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def main():
|
| 124 |
+
print("=== AI Legal Compliance Auditor & Redrafter (Production - Vector DB + Agentic AI) ===")
|
| 125 |
+
|
| 126 |
+
# Check database health
|
| 127 |
+
try:
|
| 128 |
+
from database_manager import get_database_manager
|
| 129 |
+
db_manager = get_database_manager()
|
| 130 |
+
health = db_manager.check_database_health()
|
| 131 |
+
|
| 132 |
+
if health["status"] == "empty":
|
| 133 |
+
print("\n⚠️ Warning: Vector database is empty!")
|
| 134 |
+
print(" Run 'python initialize_database.py' to index reference documents.")
|
| 135 |
+
print(" The system will use provided RAG contexts as fallback.\n")
|
| 136 |
+
elif health["status"] == "healthy":
|
| 137 |
+
print(f"✅ Vector database ready ({health['document_count']} document chunks indexed)\n")
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"⚠️ Warning: Could not check database: {e}")
|
| 140 |
+
print(" The system will use provided RAG contexts.\n")
|
| 141 |
+
|
| 142 |
+
user_document = prompt_for_document("USER DOCUMENT TO BE AUDITED")
|
| 143 |
+
rag_contexts = prompt_for_rag_contexts()
|
| 144 |
+
|
| 145 |
+
print("\nRunning compliance audit with Gemini... this may take a moment.\n")
|
| 146 |
+
|
| 147 |
+
# Check if user wants agentic mode
|
| 148 |
+
use_agentic = os.getenv("USE_AGENTIC", "true").lower() == "true"
|
| 149 |
+
use_vector_search = os.getenv("USE_VECTOR_SEARCH", "true").lower() == "true"
|
| 150 |
+
|
| 151 |
+
if use_agentic:
|
| 152 |
+
print("🤖 Agentic AI mode: ENABLED (multi-step reasoning with tools)")
|
| 153 |
+
else:
|
| 154 |
+
print("📝 Standard mode: Single-shot LLM call")
|
| 155 |
+
|
| 156 |
+
if use_vector_search:
|
| 157 |
+
print("🔍 Vector search: ENABLED (semantic search from database)")
|
| 158 |
+
else:
|
| 159 |
+
print("📄 Text search: Using provided documents only")
|
| 160 |
+
|
| 161 |
+
client = configure_gemini_model()
|
| 162 |
+
model_name = os.getenv("GEMINI_MODEL", DEFAULT_GEMINI_MODEL)
|
| 163 |
+
try:
|
| 164 |
+
result = call_llm_with_gemini(client, model_name, user_document, rag_contexts, use_agentic=use_agentic)
|
| 165 |
+
except Exception as exc:
|
| 166 |
+
print(f"\nERROR while calling Gemini model '{model_name}': {exc}\n")
|
| 167 |
+
if "NOT_FOUND" in str(exc) or "404" in str(exc) or "not found" in str(exc).lower():
|
| 168 |
+
print("❌ Model not found! The model name you specified doesn't exist.\n")
|
| 169 |
+
print("To see available models, run:")
|
| 170 |
+
print(" python list_gemini_models.py\n")
|
| 171 |
+
print("Recommended models:")
|
| 172 |
+
print(" - models/gemini-2.5-pro (stable, best quality)")
|
| 173 |
+
print(" - models/gemini-2.5-flash (faster, cheaper)")
|
| 174 |
+
print(" - models/gemini-3-pro-preview (latest preview)\n")
|
| 175 |
+
print("Set GEMINI_MODEL in your .env file to one of the available models.\n")
|
| 176 |
+
elif "RESOURCE_EXHAUSTED" in str(exc) or "429" in str(exc) or "quota" in str(exc).lower():
|
| 177 |
+
print("Your Gemini API key currently has no available quota for this model.\n")
|
| 178 |
+
print("- Enable billing / increase quota for your Gemini API project, or")
|
| 179 |
+
print("- Switch GEMINI_MODEL in your .env to a model you have quota for.\n")
|
| 180 |
+
else:
|
| 181 |
+
print("If this is a model-not-found error, run 'python list_gemini_models.py' to see available models.\n")
|
| 182 |
+
raise
|
| 183 |
+
|
| 184 |
+
# The model already includes the humanized summary as part of the JSON.
|
| 185 |
+
# We simply print the JSON and then the Markdown summary.
|
| 186 |
+
print("\n=== RAW JSON RESULT ===")
|
| 187 |
+
import json
|
| 188 |
+
|
| 189 |
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
| 190 |
+
|
| 191 |
+
human_md = result.get("humanized_summary_markdown", "")
|
| 192 |
+
if human_md:
|
| 193 |
+
print("\n---\n")
|
| 194 |
+
print(human_md)
|
| 195 |
+
|
| 196 |
+
# Optional: ask user whether to save outputs
|
| 197 |
+
save_choice = input("\nDo you want to save the JSON and Markdown to files? (y/n): ").strip().lower()
|
| 198 |
+
if save_choice == "y":
|
| 199 |
+
json_path = input("Enter path for JSON output file (e.g., audit_result.json): ").strip()
|
| 200 |
+
md_path = input("Enter path for Markdown report file (e.g., audit_report.md): ").strip()
|
| 201 |
+
|
| 202 |
+
with open(json_path, "w", encoding="utf-8") as jf:
|
| 203 |
+
json.dump(result, jf, indent=2, ensure_ascii=False)
|
| 204 |
+
|
| 205 |
+
with open(md_path, "w", encoding="utf-8") as mf:
|
| 206 |
+
mf.write(human_md)
|
| 207 |
+
|
| 208 |
+
print(f"\nSaved JSON to {json_path}")
|
| 209 |
+
print(f"Saved Markdown report to {md_path}")
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
if __name__ == "__main__":
|
| 213 |
+
main()
|
| 214 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
google-genai>=0.6.0
|
| 2 |
+
python-dotenv>=1.0.0
|
| 3 |
+
gradio>=5.0.0
|
| 4 |
+
python-docx>=1.0.0
|
| 5 |
+
pypdf>=5.0.0
|
| 6 |
+
langchain>=0.1.0
|
| 7 |
+
langchain-google-genai>=1.0.0
|
| 8 |
+
langchain-core>=0.1.0
|
| 9 |
+
langchain-community>=0.0.20
|
| 10 |
+
langchain-text-splitters>=0.0.1
|
| 11 |
+
langchain-chroma>=0.1.0
|
| 12 |
+
chromadb>=0.4.0
|
| 13 |
+
sentence-transformers>=2.2.0
|
| 14 |
+
numpy>=1.24.0
|
| 15 |
+
protobuf>=5.26.1,<6.0.0
|
| 16 |
+
# Note: If you have tensorflow-intel installed, it will show a conflict warning.
|
| 17 |
+
# This is safe to ignore - the compliance auditor doesn't require TensorFlow.
|
| 18 |
+
# To remove the warning: pip uninstall tensorflow-intel
|
| 19 |
+
|
| 20 |
+
# IMPORTANT: Do NOT install 'docx' package - it conflicts with python-docx
|
| 21 |
+
# If you see "ModuleNotFoundError: No module named 'exceptions'" error:
|
| 22 |
+
# pip uninstall docx
|
| 23 |
+
# pip install python-docx
|
retrieval.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Retrieval module for production deployment.
|
| 3 |
+
Handles semantic search and hybrid search strategies.
|
| 4 |
+
"""
|
| 5 |
+
from typing import List, Dict, Any, Optional, Tuple
|
| 6 |
+
from langchain_core.documents import Document
|
| 7 |
+
import re
|
| 8 |
+
|
| 9 |
+
from vector_db import get_vector_database
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class RetrievalSystem:
|
| 13 |
+
"""
|
| 14 |
+
Handles retrieval of relevant regulations using semantic search.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(self, vector_db=None):
|
| 18 |
+
"""
|
| 19 |
+
Initialize retrieval system.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
vector_db: VectorDatabase instance (optional)
|
| 23 |
+
"""
|
| 24 |
+
self.vector_db = vector_db or get_vector_database()
|
| 25 |
+
|
| 26 |
+
def semantic_search(
|
| 27 |
+
self,
|
| 28 |
+
query: str,
|
| 29 |
+
k: int = 5,
|
| 30 |
+
min_score: float = 0.0,
|
| 31 |
+
filter_dict: Optional[Dict[str, Any]] = None
|
| 32 |
+
) -> List[Document]:
|
| 33 |
+
"""
|
| 34 |
+
Perform semantic search for relevant regulations.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
query: Search query
|
| 38 |
+
k: Number of results
|
| 39 |
+
min_score: Minimum similarity score
|
| 40 |
+
filter_dict: Optional metadata filters
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
List of relevant Document objects
|
| 44 |
+
"""
|
| 45 |
+
if not query or not query.strip():
|
| 46 |
+
return []
|
| 47 |
+
|
| 48 |
+
# Perform semantic search
|
| 49 |
+
results_with_scores = self.vector_db.search_with_scores(
|
| 50 |
+
query,
|
| 51 |
+
k=k * 2, # Get more results for filtering
|
| 52 |
+
filter_dict=filter_dict
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Filter by minimum score and limit to k
|
| 56 |
+
filtered_results = [
|
| 57 |
+
doc for doc, score in results_with_scores
|
| 58 |
+
if score >= min_score
|
| 59 |
+
][:k]
|
| 60 |
+
|
| 61 |
+
return filtered_results
|
| 62 |
+
|
| 63 |
+
def hybrid_search(
|
| 64 |
+
self,
|
| 65 |
+
query: str,
|
| 66 |
+
k: int = 5,
|
| 67 |
+
semantic_weight: float = 0.7,
|
| 68 |
+
keyword_weight: float = 0.3,
|
| 69 |
+
filter_dict: Optional[Dict[str, Any]] = None
|
| 70 |
+
) -> List[Document]:
|
| 71 |
+
"""
|
| 72 |
+
Perform hybrid search (semantic + keyword).
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
query: Search query
|
| 76 |
+
k: Number of results
|
| 77 |
+
semantic_weight: Weight for semantic search
|
| 78 |
+
keyword_weight: Weight for keyword search
|
| 79 |
+
filter_dict: Optional filters
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
List of relevant Document objects
|
| 83 |
+
"""
|
| 84 |
+
# Semantic search
|
| 85 |
+
semantic_results = self.semantic_search(query, k=k * 2, filter_dict=filter_dict)
|
| 86 |
+
|
| 87 |
+
# Keyword search (simple implementation)
|
| 88 |
+
keyword_results = self._keyword_search(query, filter_dict=filter_dict)
|
| 89 |
+
|
| 90 |
+
# Combine and rank
|
| 91 |
+
combined = self._merge_results(
|
| 92 |
+
semantic_results,
|
| 93 |
+
keyword_results,
|
| 94 |
+
semantic_weight,
|
| 95 |
+
keyword_weight
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
return combined[:k]
|
| 99 |
+
|
| 100 |
+
def _keyword_search(
|
| 101 |
+
self,
|
| 102 |
+
query: str,
|
| 103 |
+
filter_dict: Optional[Dict[str, Any]] = None
|
| 104 |
+
) -> List[Document]:
|
| 105 |
+
"""
|
| 106 |
+
Simple keyword-based search.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
query: Search query
|
| 110 |
+
filter_dict: Optional filters
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
List of Document objects
|
| 114 |
+
"""
|
| 115 |
+
# Extract keywords
|
| 116 |
+
keywords = re.findall(r'\b\w+\b', query.lower())
|
| 117 |
+
|
| 118 |
+
# Get all documents (this is simplified - in production, use proper indexing)
|
| 119 |
+
# For now, use semantic search as fallback
|
| 120 |
+
return self.semantic_search(query, k=10, filter_dict=filter_dict)
|
| 121 |
+
|
| 122 |
+
def _merge_results(
|
| 123 |
+
self,
|
| 124 |
+
semantic_results: List[Document],
|
| 125 |
+
keyword_results: List[Document],
|
| 126 |
+
semantic_weight: float,
|
| 127 |
+
keyword_weight: float
|
| 128 |
+
) -> List[Document]:
|
| 129 |
+
"""
|
| 130 |
+
Merge and rank results from different search methods.
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
semantic_results: Results from semantic search
|
| 134 |
+
keyword_results: Results from keyword search
|
| 135 |
+
semantic_weight: Weight for semantic results
|
| 136 |
+
keyword_weight: Weight for keyword results
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
Merged and ranked results
|
| 140 |
+
"""
|
| 141 |
+
# Create score dictionary
|
| 142 |
+
doc_scores = {}
|
| 143 |
+
|
| 144 |
+
# Add semantic results
|
| 145 |
+
for idx, doc in enumerate(semantic_results):
|
| 146 |
+
doc_id = id(doc)
|
| 147 |
+
doc_scores[doc_id] = doc_scores.get(doc_id, 0) + semantic_weight * (1.0 - idx / len(semantic_results))
|
| 148 |
+
doc_scores[doc_id] = doc_scores.get(doc_id, 0) + semantic_weight
|
| 149 |
+
|
| 150 |
+
# Add keyword results
|
| 151 |
+
for idx, doc in enumerate(keyword_results):
|
| 152 |
+
doc_id = id(doc)
|
| 153 |
+
doc_scores[doc_id] = doc_scores.get(doc_id, 0) + keyword_weight * (1.0 - idx / len(keyword_results))
|
| 154 |
+
|
| 155 |
+
# Create document map
|
| 156 |
+
doc_map = {}
|
| 157 |
+
for doc in semantic_results + keyword_results:
|
| 158 |
+
doc_map[id(doc)] = doc
|
| 159 |
+
|
| 160 |
+
# Sort by score
|
| 161 |
+
sorted_docs = sorted(
|
| 162 |
+
doc_map.items(),
|
| 163 |
+
key=lambda x: doc_scores.get(x[0], 0),
|
| 164 |
+
reverse=True
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
return [doc for _, doc in sorted_docs]
|
| 168 |
+
|
| 169 |
+
def retrieve_relevant_regulations(
|
| 170 |
+
self,
|
| 171 |
+
user_document: str,
|
| 172 |
+
k: int = 5,
|
| 173 |
+
use_hybrid: bool = True
|
| 174 |
+
) -> List[Tuple[str, str]]:
|
| 175 |
+
"""
|
| 176 |
+
Retrieve relevant regulations for a user document.
|
| 177 |
+
|
| 178 |
+
Args:
|
| 179 |
+
user_document: User's document text
|
| 180 |
+
k: Number of regulations to retrieve
|
| 181 |
+
use_hybrid: Whether to use hybrid search
|
| 182 |
+
|
| 183 |
+
Returns:
|
| 184 |
+
List of (label, text) tuples
|
| 185 |
+
"""
|
| 186 |
+
if not user_document or not user_document.strip():
|
| 187 |
+
return []
|
| 188 |
+
|
| 189 |
+
# Extract key phrases from user document for search
|
| 190 |
+
# Use first 500 chars as query (or full doc if shorter)
|
| 191 |
+
query = user_document[:500] if len(user_document) > 500 else user_document
|
| 192 |
+
|
| 193 |
+
# Perform search
|
| 194 |
+
if use_hybrid:
|
| 195 |
+
results = self.hybrid_search(query, k=k)
|
| 196 |
+
else:
|
| 197 |
+
results = self.semantic_search(query, k=k)
|
| 198 |
+
|
| 199 |
+
# Convert to (label, text) format
|
| 200 |
+
regulations = []
|
| 201 |
+
seen_sources = set()
|
| 202 |
+
|
| 203 |
+
for doc in results:
|
| 204 |
+
source = doc.metadata.get("source", "Unknown")
|
| 205 |
+
|
| 206 |
+
# Avoid duplicates from same source
|
| 207 |
+
if source in seen_sources:
|
| 208 |
+
continue
|
| 209 |
+
|
| 210 |
+
seen_sources.add(source)
|
| 211 |
+
regulations.append((source, doc.page_content))
|
| 212 |
+
|
| 213 |
+
return regulations
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
# Global instance
|
| 217 |
+
_retrieval_system: Optional[RetrievalSystem] = None
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def get_retrieval_system() -> RetrievalSystem:
|
| 221 |
+
"""Get or create global retrieval system instance."""
|
| 222 |
+
global _retrieval_system
|
| 223 |
+
if _retrieval_system is None:
|
| 224 |
+
_retrieval_system = RetrievalSystem()
|
| 225 |
+
return _retrieval_system
|
vector_db.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Vector database module for production deployment.
|
| 3 |
+
Uses ChromaDB for storing and retrieving regulation embeddings.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
from typing import List, Dict, Any, Optional, Tuple
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
import chromadb
|
| 11 |
+
from chromadb.config import Settings
|
| 12 |
+
except ImportError:
|
| 13 |
+
chromadb = None
|
| 14 |
+
Settings = None
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
from langchain_chroma import Chroma
|
| 18 |
+
except ImportError:
|
| 19 |
+
try:
|
| 20 |
+
from langchain.vectorstores import Chroma
|
| 21 |
+
except ImportError:
|
| 22 |
+
try:
|
| 23 |
+
from langchain_community.vectorstores import Chroma
|
| 24 |
+
except ImportError:
|
| 25 |
+
Chroma = None
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
from langchain_core.documents import Document
|
| 29 |
+
except ImportError:
|
| 30 |
+
try:
|
| 31 |
+
from langchain_core.documents import Document
|
| 32 |
+
except ImportError:
|
| 33 |
+
Document = None
|
| 34 |
+
|
| 35 |
+
from embeddings import get_embedding_generator
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class VectorDatabase:
|
| 39 |
+
"""
|
| 40 |
+
Manages vector database operations for regulation storage and retrieval.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(
|
| 44 |
+
self,
|
| 45 |
+
persist_directory: str = "./chroma_db",
|
| 46 |
+
collection_name: str = "regulations"
|
| 47 |
+
):
|
| 48 |
+
"""
|
| 49 |
+
Initialize vector database.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
persist_directory: Directory to persist database
|
| 53 |
+
collection_name: Name of the collection
|
| 54 |
+
"""
|
| 55 |
+
if chromadb is None:
|
| 56 |
+
raise RuntimeError(
|
| 57 |
+
"chromadb not installed. Run: pip install chromadb"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
if Chroma is None:
|
| 61 |
+
raise RuntimeError(
|
| 62 |
+
"langchain.vectorstores.Chroma not available. Run: pip install langchain langchain-community"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
self.persist_directory = persist_directory
|
| 66 |
+
self.collection_name = collection_name
|
| 67 |
+
|
| 68 |
+
# Ensure directory exists
|
| 69 |
+
Path(persist_directory).mkdir(parents=True, exist_ok=True)
|
| 70 |
+
|
| 71 |
+
# Initialize embeddings
|
| 72 |
+
self.embedding_generator = get_embedding_generator()
|
| 73 |
+
|
| 74 |
+
# Initialize ChromaDB
|
| 75 |
+
self._initialize_database()
|
| 76 |
+
|
| 77 |
+
def _initialize_database(self):
|
| 78 |
+
"""Initialize ChromaDB with embeddings."""
|
| 79 |
+
try:
|
| 80 |
+
# Create vector store
|
| 81 |
+
self.vector_store = Chroma(
|
| 82 |
+
persist_directory=self.persist_directory,
|
| 83 |
+
collection_name=self.collection_name,
|
| 84 |
+
embedding_function=self.embedding_generator.embeddings
|
| 85 |
+
)
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f"⚠️ Warning: Failed to load existing database: {e}")
|
| 88 |
+
# Create new database
|
| 89 |
+
self.vector_store = Chroma(
|
| 90 |
+
persist_directory=self.persist_directory,
|
| 91 |
+
collection_name=self.collection_name,
|
| 92 |
+
embedding_function=self.embedding_generator.embeddings
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
def add_documents(
|
| 96 |
+
self,
|
| 97 |
+
documents: List[Document],
|
| 98 |
+
batch_size: int = 100
|
| 99 |
+
) -> List[str]:
|
| 100 |
+
"""
|
| 101 |
+
Add documents to the vector database.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
documents: List of Document objects
|
| 105 |
+
batch_size: Number of documents to add at once
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
List of document IDs
|
| 109 |
+
"""
|
| 110 |
+
if not documents:
|
| 111 |
+
return []
|
| 112 |
+
|
| 113 |
+
ids = []
|
| 114 |
+
|
| 115 |
+
# Add in batches
|
| 116 |
+
for i in range(0, len(documents), batch_size):
|
| 117 |
+
batch = documents[i:i + batch_size]
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
batch_ids = self.vector_store.add_documents(batch)
|
| 121 |
+
ids.extend(batch_ids)
|
| 122 |
+
except Exception as e:
|
| 123 |
+
print(f"⚠️ Warning: Failed to add batch {i//batch_size + 1}: {e}")
|
| 124 |
+
|
| 125 |
+
# Persist changes
|
| 126 |
+
self.vector_store.persist()
|
| 127 |
+
|
| 128 |
+
return ids
|
| 129 |
+
|
| 130 |
+
def search(
|
| 131 |
+
self,
|
| 132 |
+
query: str,
|
| 133 |
+
k: int = 5,
|
| 134 |
+
filter_dict: Optional[Dict[str, Any]] = None
|
| 135 |
+
) -> List[Document]:
|
| 136 |
+
"""
|
| 137 |
+
Search for similar documents using semantic search.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
query: Search query
|
| 141 |
+
k: Number of results to return
|
| 142 |
+
filter_dict: Optional metadata filters
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
List of similar Document objects
|
| 146 |
+
"""
|
| 147 |
+
if not query or not query.strip():
|
| 148 |
+
return []
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
if filter_dict:
|
| 152 |
+
results = self.vector_store.similarity_search(
|
| 153 |
+
query,
|
| 154 |
+
k=k,
|
| 155 |
+
filter=filter_dict
|
| 156 |
+
)
|
| 157 |
+
else:
|
| 158 |
+
results = self.vector_store.similarity_search(query, k=k)
|
| 159 |
+
|
| 160 |
+
return results
|
| 161 |
+
except Exception as e:
|
| 162 |
+
print(f"⚠️ Warning: Search failed: {e}")
|
| 163 |
+
return []
|
| 164 |
+
|
| 165 |
+
def search_with_scores(
|
| 166 |
+
self,
|
| 167 |
+
query: str,
|
| 168 |
+
k: int = 5,
|
| 169 |
+
filter_dict: Optional[Dict[str, Any]] = None
|
| 170 |
+
) -> List[Tuple[Document, float]]:
|
| 171 |
+
"""
|
| 172 |
+
Search with similarity scores.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
query: Search query
|
| 176 |
+
k: Number of results
|
| 177 |
+
filter_dict: Optional filters
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
List of (Document, score) tuples
|
| 181 |
+
"""
|
| 182 |
+
if not query or not query.strip():
|
| 183 |
+
return []
|
| 184 |
+
|
| 185 |
+
try:
|
| 186 |
+
if filter_dict:
|
| 187 |
+
results = self.vector_store.similarity_search_with_score(
|
| 188 |
+
query,
|
| 189 |
+
k=k,
|
| 190 |
+
filter=filter_dict
|
| 191 |
+
)
|
| 192 |
+
else:
|
| 193 |
+
results = self.vector_store.similarity_search_with_score(query, k=k)
|
| 194 |
+
|
| 195 |
+
return results
|
| 196 |
+
except Exception as e:
|
| 197 |
+
print(f"⚠️ Warning: Search with scores failed: {e}")
|
| 198 |
+
return []
|
| 199 |
+
|
| 200 |
+
def delete_documents(
|
| 201 |
+
self,
|
| 202 |
+
ids: Optional[List[str]] = None,
|
| 203 |
+
filter_dict: Optional[Dict[str, Any]] = None
|
| 204 |
+
) -> bool:
|
| 205 |
+
"""
|
| 206 |
+
Delete documents from the database.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
ids: List of document IDs to delete
|
| 210 |
+
filter_dict: Optional metadata filters
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
True if successful
|
| 214 |
+
"""
|
| 215 |
+
try:
|
| 216 |
+
if ids:
|
| 217 |
+
self.vector_store.delete(ids=ids)
|
| 218 |
+
elif filter_dict:
|
| 219 |
+
# ChromaDB doesn't support filter-based delete directly
|
| 220 |
+
# Need to find IDs first
|
| 221 |
+
all_docs = self.vector_store.get()
|
| 222 |
+
# This is a simplified version - full implementation would filter
|
| 223 |
+
pass
|
| 224 |
+
|
| 225 |
+
self.vector_store.persist()
|
| 226 |
+
return True
|
| 227 |
+
except Exception as e:
|
| 228 |
+
print(f"⚠️ Warning: Delete failed: {e}")
|
| 229 |
+
return False
|
| 230 |
+
|
| 231 |
+
def get_collection_info(self) -> Dict[str, Any]:
|
| 232 |
+
"""
|
| 233 |
+
Get information about the collection.
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
Dictionary with collection statistics
|
| 237 |
+
"""
|
| 238 |
+
try:
|
| 239 |
+
collection = self.vector_store._collection
|
| 240 |
+
count = collection.count()
|
| 241 |
+
|
| 242 |
+
return {
|
| 243 |
+
"collection_name": self.collection_name,
|
| 244 |
+
"document_count": count,
|
| 245 |
+
"persist_directory": self.persist_directory
|
| 246 |
+
}
|
| 247 |
+
except Exception as e:
|
| 248 |
+
return {
|
| 249 |
+
"error": str(e),
|
| 250 |
+
"collection_name": self.collection_name
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
def clear_collection(self) -> bool:
|
| 254 |
+
"""
|
| 255 |
+
Clear all documents from the collection.
|
| 256 |
+
|
| 257 |
+
Returns:
|
| 258 |
+
True if successful
|
| 259 |
+
"""
|
| 260 |
+
try:
|
| 261 |
+
# Delete the collection and recreate
|
| 262 |
+
import shutil
|
| 263 |
+
if os.path.exists(self.persist_directory):
|
| 264 |
+
shutil.rmtree(self.persist_directory)
|
| 265 |
+
Path(self.persist_directory).mkdir(parents=True, exist_ok=True)
|
| 266 |
+
self._initialize_database()
|
| 267 |
+
return True
|
| 268 |
+
except Exception as e:
|
| 269 |
+
print(f"⚠️ Warning: Clear collection failed: {e}")
|
| 270 |
+
return False
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
# Global instance
|
| 274 |
+
_vector_db: Optional[VectorDatabase] = None
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def get_vector_database() -> VectorDatabase:
|
| 278 |
+
"""Get or create global vector database instance."""
|
| 279 |
+
global _vector_db
|
| 280 |
+
if _vector_db is None:
|
| 281 |
+
_vector_db = VectorDatabase()
|
| 282 |
+
return _vector_db
|