Kushal Shah Claude Sonnet 4.6 commited on
Commit
bd510a2
·
0 Parent(s):

Initial commit: AI Legal Compliance Auditor

Browse files

Gradio-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 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