Spaces:
Runtime error
Runtime error
Harsh Upadhyay
commited on
Commit
Β·
89a2809
1
Parent(s):
7ce8c97
added postgres and updated routes.
Browse files- backend/app/__init__.py +0 -4
- backend/app/database.py +185 -266
- backend/app/routes/routes.py +67 -276
- backend/create_db.py +0 -17
- backend/requirements.txt +0 -0
backend/app/__init__.py
CHANGED
@@ -3,7 +3,6 @@ from flask import Flask
|
|
3 |
from flask_jwt_extended import JWTManager
|
4 |
from flask_cors import CORS
|
5 |
from app.routes.routes import main # β
Make sure this works
|
6 |
-
from app.database import init_db
|
7 |
import logging
|
8 |
|
9 |
jwt = JWTManager()
|
@@ -19,9 +18,6 @@ def create_app(config_object):
|
|
19 |
handler.setFormatter(formatter)
|
20 |
app.logger.addHandler(handler)
|
21 |
|
22 |
-
# π§± Initialize DB
|
23 |
-
init_db()
|
24 |
-
|
25 |
# π Initialize JWT
|
26 |
jwt.init_app(app)
|
27 |
|
|
|
3 |
from flask_jwt_extended import JWTManager
|
4 |
from flask_cors import CORS
|
5 |
from app.routes.routes import main # β
Make sure this works
|
|
|
6 |
import logging
|
7 |
|
8 |
jwt = JWTManager()
|
|
|
18 |
handler.setFormatter(formatter)
|
19 |
app.logger.addHandler(handler)
|
20 |
|
|
|
|
|
|
|
21 |
# π Initialize JWT
|
22 |
jwt.init_app(app)
|
23 |
|
backend/app/database.py
CHANGED
@@ -1,314 +1,233 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
import logging
|
4 |
-
from datetime import datetime
|
5 |
-
import json
|
6 |
-
|
7 |
-
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
8 |
-
DB_PATH = os.path.join(BASE_DIR, 'legal_docs.db')
|
9 |
-
|
10 |
-
def init_db():
|
11 |
-
"""Initialize the database with required tables"""
|
12 |
-
try:
|
13 |
-
conn = sqlite3.connect(DB_PATH)
|
14 |
-
cursor = conn.cursor()
|
15 |
-
|
16 |
-
# Create users table
|
17 |
-
cursor.execute('''
|
18 |
-
CREATE TABLE IF NOT EXISTS users (
|
19 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
20 |
-
username TEXT UNIQUE NOT NULL,
|
21 |
-
email TEXT UNIQUE NOT NULL,
|
22 |
-
password_hash TEXT NOT NULL,
|
23 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
24 |
-
)
|
25 |
-
''')
|
26 |
-
|
27 |
-
# Create documents table
|
28 |
-
cursor.execute('''
|
29 |
-
CREATE TABLE IF NOT EXISTS documents (
|
30 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
31 |
-
title TEXT NOT NULL,
|
32 |
-
full_text TEXT,
|
33 |
-
summary TEXT,
|
34 |
-
clauses TEXT,
|
35 |
-
features TEXT,
|
36 |
-
context_analysis TEXT,
|
37 |
-
file_path TEXT,
|
38 |
-
upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
39 |
-
)
|
40 |
-
''')
|
41 |
-
|
42 |
-
# Create question_answers table for persisting Q&A
|
43 |
-
cursor.execute('''
|
44 |
-
CREATE TABLE IF NOT EXISTS question_answers (
|
45 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
46 |
-
document_id INTEGER NOT NULL,
|
47 |
-
user_id INTEGER NOT NULL,
|
48 |
-
question TEXT NOT NULL,
|
49 |
-
answer TEXT NOT NULL,
|
50 |
-
score REAL DEFAULT 0.0,
|
51 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
52 |
-
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
|
53 |
-
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
54 |
-
)
|
55 |
-
''')
|
56 |
-
|
57 |
-
conn.commit()
|
58 |
-
logging.info("Database initialized successfully")
|
59 |
-
except Exception as e:
|
60 |
-
logging.error(f"Error initializing database: {str(e)}")
|
61 |
-
raise
|
62 |
-
finally:
|
63 |
-
conn.close()
|
64 |
-
|
65 |
-
def get_db_connection():
|
66 |
-
"""Get a database connection"""
|
67 |
-
conn = sqlite3.connect(DB_PATH)
|
68 |
-
conn.row_factory = sqlite3.Row
|
69 |
-
return conn
|
70 |
-
|
71 |
-
# Initialize database when module is imported
|
72 |
-
init_db()
|
73 |
-
|
74 |
-
def search_documents(query, search_type='all'):
|
75 |
-
conn = sqlite3.connect(DB_PATH)
|
76 |
-
c = conn.cursor()
|
77 |
-
|
78 |
-
try:
|
79 |
-
# Check if query is a number (potential ID)
|
80 |
-
is_id_search = query.isdigit()
|
81 |
-
|
82 |
-
if is_id_search:
|
83 |
-
# Search by ID
|
84 |
-
c.execute('''
|
85 |
-
SELECT id, title, summary, upload_time, 1.0 as match_score
|
86 |
-
FROM documents
|
87 |
-
WHERE id = ?
|
88 |
-
''', (int(query),))
|
89 |
-
else:
|
90 |
-
# Search by title
|
91 |
-
c.execute('''
|
92 |
-
SELECT id, title, summary, upload_time, 1.0 as match_score
|
93 |
-
FROM documents
|
94 |
-
WHERE title LIKE ?
|
95 |
-
ORDER BY id DESC
|
96 |
-
''', (f'%{query}%',))
|
97 |
-
|
98 |
-
results = []
|
99 |
-
for row in c.fetchall():
|
100 |
-
results.append({
|
101 |
-
"id": row[0],
|
102 |
-
"title": row[1],
|
103 |
-
"summary": row[2] or "",
|
104 |
-
"upload_time": row[3],
|
105 |
-
"match_score": row[4]
|
106 |
-
})
|
107 |
-
|
108 |
-
return results
|
109 |
-
except sqlite3.Error as e:
|
110 |
-
logging.error(f"Search error: {str(e)}")
|
111 |
-
raise
|
112 |
-
finally:
|
113 |
-
conn.close()
|
114 |
-
|
115 |
-
def migrate_add_user_id_to_documents():
|
116 |
-
"""Add user_id column to documents table if it doesn't exist."""
|
117 |
-
try:
|
118 |
-
conn = sqlite3.connect(DB_PATH)
|
119 |
-
cursor = conn.cursor()
|
120 |
-
# Check if user_id column exists
|
121 |
-
cursor.execute("PRAGMA table_info(documents)")
|
122 |
-
columns = [row[1] for row in cursor.fetchall()]
|
123 |
-
if 'user_id' not in columns:
|
124 |
-
cursor.execute('ALTER TABLE documents ADD COLUMN user_id INTEGER')
|
125 |
-
conn.commit()
|
126 |
-
logging.info("Added user_id column to documents table.")
|
127 |
-
except Exception as e:
|
128 |
-
logging.error(f"Migration error: {str(e)}")
|
129 |
-
raise
|
130 |
-
finally:
|
131 |
-
conn.close()
|
132 |
-
|
133 |
-
# Call migration on import
|
134 |
-
migrate_add_user_id_to_documents()
|
135 |
-
|
136 |
-
def migrate_add_phone_company_to_users():
|
137 |
-
"""Add phone and company columns to users table if they don't exist."""
|
138 |
-
try:
|
139 |
-
conn = sqlite3.connect(DB_PATH)
|
140 |
-
cursor = conn.cursor()
|
141 |
-
cursor.execute("PRAGMA table_info(users)")
|
142 |
-
columns = [row[1] for row in cursor.fetchall()]
|
143 |
-
if 'phone' not in columns:
|
144 |
-
cursor.execute('ALTER TABLE users ADD COLUMN phone TEXT')
|
145 |
-
if 'company' not in columns:
|
146 |
-
cursor.execute('ALTER TABLE users ADD COLUMN company TEXT')
|
147 |
-
conn.commit()
|
148 |
-
except Exception as e:
|
149 |
-
logging.error(f"Migration error: {str(e)}")
|
150 |
-
raise
|
151 |
-
finally:
|
152 |
-
conn.close()
|
153 |
-
|
154 |
-
# Call migration on import
|
155 |
-
migrate_add_phone_company_to_users()
|
156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
157 |
def save_document(title, full_text, summary, clauses, features, context_analysis, file_path, user_id):
|
158 |
-
|
159 |
try:
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
|
|
|
|
|
|
|
|
|
|
168 |
except Exception as e:
|
169 |
-
|
170 |
raise
|
171 |
finally:
|
172 |
-
|
173 |
|
174 |
def get_all_documents(user_id=None):
|
175 |
-
|
176 |
try:
|
177 |
-
|
178 |
-
cursor = conn.cursor()
|
179 |
if user_id is not None:
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
documents = [dict(row) for row in cursor.fetchall()]
|
184 |
for doc in documents:
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
doc['size'] = None
|
190 |
-
return documents
|
191 |
-
except Exception as e:
|
192 |
-
logging.error(f"Error fetching documents: {str(e)}")
|
193 |
-
raise
|
194 |
finally:
|
195 |
-
|
196 |
|
197 |
def get_document_by_id(doc_id, user_id=None):
|
198 |
-
|
199 |
try:
|
200 |
-
|
201 |
-
cursor = conn.cursor()
|
202 |
if user_id is not None:
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
raise
|
211 |
finally:
|
212 |
-
|
213 |
|
214 |
def delete_document(doc_id):
|
215 |
-
|
216 |
try:
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
file_path = row[0] if row and row[0] else None
|
223 |
-
# Now delete the document
|
224 |
-
cursor.execute('DELETE FROM documents WHERE id = ?', (doc_id,))
|
225 |
-
conn.commit()
|
226 |
return file_path
|
227 |
-
except Exception as e:
|
228 |
-
logging.error(f"Error deleting document {doc_id}: {str(e)}")
|
229 |
-
raise
|
230 |
finally:
|
231 |
-
|
232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
def search_questions_answers(query, user_id=None):
|
234 |
-
|
235 |
-
c = conn.cursor()
|
236 |
try:
|
237 |
-
|
238 |
-
SELECT id, document_id, question, answer, created_at
|
239 |
-
FROM question_answers
|
240 |
-
WHERE (question LIKE ? OR answer LIKE ?)
|
241 |
-
'''
|
242 |
-
params = [f'%{query}%', f'%{query}%']
|
243 |
if user_id is not None:
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
c.execute(sql, params)
|
248 |
results = []
|
249 |
-
for row in
|
250 |
results.append({
|
251 |
-
'id': row
|
252 |
-
'document_id': row
|
253 |
-
'question': row
|
254 |
-
'answer': row
|
255 |
-
'created_at': row
|
256 |
})
|
257 |
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
258 |
except Exception as e:
|
259 |
-
|
260 |
raise
|
261 |
finally:
|
262 |
-
|
263 |
|
|
|
264 |
def get_user_profile(username):
|
265 |
-
|
266 |
try:
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
|
|
275 |
finally:
|
276 |
-
|
277 |
|
278 |
def update_user_profile(username, email, phone, company):
|
279 |
-
|
280 |
try:
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
logging.error(f"Error updating user profile: {str(e)}")
|
290 |
-
raise
|
291 |
finally:
|
292 |
-
|
293 |
|
294 |
def change_user_password(username, current_password, new_password):
|
295 |
-
|
296 |
try:
|
297 |
-
|
298 |
-
|
299 |
-
cursor.execute('SELECT password_hash FROM users WHERE username = ?', (username,))
|
300 |
-
row = cursor.fetchone()
|
301 |
-
if not row:
|
302 |
return False, 'User not found'
|
303 |
-
|
304 |
-
if not check_password_hash(row[0], current_password):
|
305 |
return False, 'Current password is incorrect'
|
306 |
-
|
307 |
-
|
308 |
-
conn.commit()
|
309 |
return True, 'Password updated successfully'
|
310 |
-
except Exception as e:
|
311 |
-
logging.error(f"Error changing password: {str(e)}")
|
312 |
-
raise
|
313 |
finally:
|
314 |
-
|
|
|
1 |
+
# All sqlite3 and local DB logic will be removed and replaced with SQLAlchemy/Postgres in the next step.
|
2 |
+
# This file will be refactored to use SQLAlchemy models and sessions.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
+
from sqlalchemy import create_engine, Column, Integer, String, Text, Float, ForeignKey, DateTime
|
5 |
+
from sqlalchemy.orm import declarative_base, sessionmaker, relationship
|
6 |
+
from sqlalchemy.sql import func
|
7 |
+
import os
|
8 |
+
from sqlalchemy.exc import IntegrityError
|
9 |
+
from werkzeug.security import check_password_hash, generate_password_hash
|
10 |
+
|
11 |
+
# SQLAlchemy setup
|
12 |
+
DATABASE_URL = os.environ.get('DATABASE_URL')
|
13 |
+
engine = create_engine(DATABASE_URL)
|
14 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
15 |
+
Base = declarative_base()
|
16 |
+
|
17 |
+
# User model
|
18 |
+
class User(Base):
|
19 |
+
__tablename__ = 'users'
|
20 |
+
id = Column(Integer, primary_key=True, index=True)
|
21 |
+
username = Column(String, unique=True, nullable=False, index=True)
|
22 |
+
email = Column(String, unique=True, nullable=False, index=True)
|
23 |
+
password_hash = Column(String, nullable=False)
|
24 |
+
phone = Column(String)
|
25 |
+
company = Column(String)
|
26 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
27 |
+
documents = relationship('Document', back_populates='user')
|
28 |
+
question_answers = relationship('QuestionAnswer', back_populates='user')
|
29 |
+
|
30 |
+
# Document model
|
31 |
+
class Document(Base):
|
32 |
+
__tablename__ = 'documents'
|
33 |
+
id = Column(Integer, primary_key=True, index=True)
|
34 |
+
title = Column(String, nullable=False)
|
35 |
+
full_text = Column(Text)
|
36 |
+
summary = Column(Text)
|
37 |
+
clauses = Column(Text)
|
38 |
+
features = Column(Text)
|
39 |
+
context_analysis = Column(Text)
|
40 |
+
file_path = Column(String)
|
41 |
+
upload_time = Column(DateTime(timezone=True), server_default=func.now())
|
42 |
+
user_id = Column(Integer, ForeignKey('users.id'))
|
43 |
+
user = relationship('User', back_populates='documents')
|
44 |
+
question_answers = relationship('QuestionAnswer', back_populates='document')
|
45 |
+
|
46 |
+
# QuestionAnswer model
|
47 |
+
class QuestionAnswer(Base):
|
48 |
+
__tablename__ = 'question_answers'
|
49 |
+
id = Column(Integer, primary_key=True, index=True)
|
50 |
+
document_id = Column(Integer, ForeignKey('documents.id'), nullable=False)
|
51 |
+
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
52 |
+
question = Column(Text, nullable=False)
|
53 |
+
answer = Column(Text, nullable=False)
|
54 |
+
score = Column(Float, default=0.0)
|
55 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
56 |
+
document = relationship('Document', back_populates='question_answers')
|
57 |
+
user = relationship('User', back_populates='question_answers')
|
58 |
+
|
59 |
+
# Create tables if they don't exist
|
60 |
+
Base.metadata.create_all(bind=engine)
|
61 |
+
|
62 |
+
def get_db_session():
|
63 |
+
return SessionLocal()
|
64 |
+
|
65 |
+
# --- Document CRUD ---
|
66 |
def save_document(title, full_text, summary, clauses, features, context_analysis, file_path, user_id):
|
67 |
+
session = get_db_session()
|
68 |
try:
|
69 |
+
doc = Document(
|
70 |
+
title=title,
|
71 |
+
full_text=full_text,
|
72 |
+
summary=summary,
|
73 |
+
clauses=str(clauses),
|
74 |
+
features=str(features),
|
75 |
+
context_analysis=str(context_analysis),
|
76 |
+
file_path=file_path,
|
77 |
+
user_id=user_id
|
78 |
+
)
|
79 |
+
session.add(doc)
|
80 |
+
session.commit()
|
81 |
+
return doc.id
|
82 |
except Exception as e:
|
83 |
+
session.rollback()
|
84 |
raise
|
85 |
finally:
|
86 |
+
session.close()
|
87 |
|
88 |
def get_all_documents(user_id=None):
|
89 |
+
session = get_db_session()
|
90 |
try:
|
91 |
+
query = session.query(Document)
|
|
|
92 |
if user_id is not None:
|
93 |
+
query = query.filter(Document.user_id == user_id)
|
94 |
+
documents = query.order_by(Document.upload_time.desc()).all()
|
95 |
+
result = []
|
|
|
96 |
for doc in documents:
|
97 |
+
d = doc.__dict__.copy()
|
98 |
+
d.pop('_sa_instance_state', None)
|
99 |
+
result.append(d)
|
100 |
+
return result
|
|
|
|
|
|
|
|
|
|
|
101 |
finally:
|
102 |
+
session.close()
|
103 |
|
104 |
def get_document_by_id(doc_id, user_id=None):
|
105 |
+
session = get_db_session()
|
106 |
try:
|
107 |
+
query = session.query(Document).filter(Document.id == doc_id)
|
|
|
108 |
if user_id is not None:
|
109 |
+
query = query.filter(Document.user_id == user_id)
|
110 |
+
doc = query.first()
|
111 |
+
if doc:
|
112 |
+
d = doc.__dict__.copy()
|
113 |
+
d.pop('_sa_instance_state', None)
|
114 |
+
return d
|
115 |
+
return None
|
|
|
116 |
finally:
|
117 |
+
session.close()
|
118 |
|
119 |
def delete_document(doc_id):
|
120 |
+
session = get_db_session()
|
121 |
try:
|
122 |
+
doc = session.query(Document).filter(Document.id == doc_id).first()
|
123 |
+
file_path = doc.file_path if doc else None
|
124 |
+
if doc:
|
125 |
+
session.delete(doc)
|
126 |
+
session.commit()
|
|
|
|
|
|
|
|
|
127 |
return file_path
|
|
|
|
|
|
|
128 |
finally:
|
129 |
+
session.close()
|
130 |
|
131 |
+
def search_documents(query, search_type='all'):
|
132 |
+
session = get_db_session()
|
133 |
+
try:
|
134 |
+
results = []
|
135 |
+
if query.isdigit():
|
136 |
+
docs = session.query(Document).filter(Document.id == int(query)).all()
|
137 |
+
else:
|
138 |
+
docs = session.query(Document).filter(Document.title.ilike(f'%{query}%')).order_by(Document.id.desc()).all()
|
139 |
+
for doc in docs:
|
140 |
+
results.append({
|
141 |
+
"id": doc.id,
|
142 |
+
"title": doc.title,
|
143 |
+
"summary": doc.summary or "",
|
144 |
+
"upload_time": doc.upload_time,
|
145 |
+
"match_score": 1.0
|
146 |
+
})
|
147 |
+
return results
|
148 |
+
finally:
|
149 |
+
session.close()
|
150 |
+
|
151 |
+
# --- Q&A ---
|
152 |
def search_questions_answers(query, user_id=None):
|
153 |
+
session = get_db_session()
|
|
|
154 |
try:
|
155 |
+
q = session.query(QuestionAnswer)
|
|
|
|
|
|
|
|
|
|
|
156 |
if user_id is not None:
|
157 |
+
q = q.filter(QuestionAnswer.user_id == user_id)
|
158 |
+
q = q.filter((QuestionAnswer.question.ilike(f'%{query}%')) | (QuestionAnswer.answer.ilike(f'%{query}%')))
|
159 |
+
q = q.order_by(QuestionAnswer.created_at.desc())
|
|
|
160 |
results = []
|
161 |
+
for row in q.all():
|
162 |
results.append({
|
163 |
+
'id': row.id,
|
164 |
+
'document_id': row.document_id,
|
165 |
+
'question': row.question,
|
166 |
+
'answer': row.answer,
|
167 |
+
'created_at': row.created_at
|
168 |
})
|
169 |
return results
|
170 |
+
finally:
|
171 |
+
session.close()
|
172 |
+
|
173 |
+
def save_question_answer(document_id, user_id, question, answer, score):
|
174 |
+
session = get_db_session()
|
175 |
+
try:
|
176 |
+
qa = QuestionAnswer(
|
177 |
+
document_id=document_id,
|
178 |
+
user_id=user_id,
|
179 |
+
question=question,
|
180 |
+
answer=answer,
|
181 |
+
score=score
|
182 |
+
)
|
183 |
+
session.add(qa)
|
184 |
+
session.commit()
|
185 |
except Exception as e:
|
186 |
+
session.rollback()
|
187 |
raise
|
188 |
finally:
|
189 |
+
session.close()
|
190 |
|
191 |
+
# --- User Profile ---
|
192 |
def get_user_profile(username):
|
193 |
+
session = get_db_session()
|
194 |
try:
|
195 |
+
user = session.query(User).filter(User.username == username).first()
|
196 |
+
if user:
|
197 |
+
return {
|
198 |
+
'username': user.username,
|
199 |
+
'email': user.email,
|
200 |
+
'phone': user.phone,
|
201 |
+
'company': user.company
|
202 |
+
}
|
203 |
+
return None
|
204 |
finally:
|
205 |
+
session.close()
|
206 |
|
207 |
def update_user_profile(username, email, phone, company):
|
208 |
+
session = get_db_session()
|
209 |
try:
|
210 |
+
user = session.query(User).filter(User.username == username).first()
|
211 |
+
if user:
|
212 |
+
user.email = email
|
213 |
+
user.phone = phone
|
214 |
+
user.company = company
|
215 |
+
session.commit()
|
216 |
+
return True
|
217 |
+
return False
|
|
|
|
|
218 |
finally:
|
219 |
+
session.close()
|
220 |
|
221 |
def change_user_password(username, current_password, new_password):
|
222 |
+
session = get_db_session()
|
223 |
try:
|
224 |
+
user = session.query(User).filter(User.username == username).first()
|
225 |
+
if not user:
|
|
|
|
|
|
|
226 |
return False, 'User not found'
|
227 |
+
if not check_password_hash(user.password_hash, current_password):
|
|
|
228 |
return False, 'Current password is incorrect'
|
229 |
+
user.password_hash = generate_password_hash(new_password)
|
230 |
+
session.commit()
|
|
|
231 |
return True, 'Password updated successfully'
|
|
|
|
|
|
|
232 |
finally:
|
233 |
+
session.close()
|
backend/app/routes/routes.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
import os
|
2 |
-
import sqlite3
|
3 |
from flask import Blueprint, request, jsonify, send_from_directory, current_app
|
4 |
from werkzeug.utils import secure_filename
|
5 |
from app.utils.extract_text import extract_text_from_pdf
|
@@ -7,7 +6,7 @@ from app.utils.summarizer import generate_summary
|
|
7 |
from app.utils.clause_detector import detect_clauses
|
8 |
from app.database import save_document, delete_document
|
9 |
from app.database import get_all_documents, get_document_by_id
|
10 |
-
from app.database import search_documents
|
11 |
from app.nlp.qa import answer_question
|
12 |
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, exceptions as jwt_exceptions
|
13 |
from flask_jwt_extended.exceptions import JWTDecodeError as JWTError
|
@@ -19,6 +18,9 @@ from app.utils.context_understanding import ContextUnderstanding
|
|
19 |
import logging
|
20 |
import textract
|
21 |
from app.database import get_user_profile, update_user_profile, change_user_password
|
|
|
|
|
|
|
22 |
|
23 |
main = Blueprint("main", __name__)
|
24 |
|
@@ -28,7 +30,6 @@ legal_domain_processor = LegalDomainFeatures()
|
|
28 |
context_processor = ContextUnderstanding()
|
29 |
|
30 |
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
31 |
-
DB_PATH = os.path.join(BASE_DIR, 'legal_docs.db')
|
32 |
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
|
33 |
|
34 |
# Ensure the upload folder exists
|
@@ -53,41 +54,35 @@ def extract_text_from_file(file_path):
|
|
53 |
else:
|
54 |
raise Exception("Unsupported file type for text extraction.")
|
55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
@main.route('/upload', methods=['POST'])
|
57 |
@jwt_required()
|
58 |
def upload_file():
|
59 |
try:
|
60 |
if 'file' not in request.files:
|
61 |
return jsonify({'error': 'No file part'}), 400
|
62 |
-
|
63 |
file = request.files['file']
|
64 |
-
if file.filename == '':
|
65 |
return jsonify({'error': 'No selected file'}), 400
|
66 |
-
|
67 |
-
# Only allow PDF files
|
68 |
if not (file.filename.lower().endswith('.pdf')):
|
69 |
return jsonify({'error': 'File type not allowed. Only PDF files are supported.'}), 400
|
70 |
-
|
71 |
-
# Save file first
|
72 |
filename = secure_filename(file.filename)
|
73 |
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
74 |
file.save(file_path)
|
75 |
-
|
76 |
-
# Get user_id from JWT identity
|
77 |
identity = get_jwt_identity()
|
78 |
-
|
79 |
-
|
80 |
-
cursor.execute('SELECT id FROM users WHERE username = ?', (identity,))
|
81 |
-
user_row = cursor.fetchone()
|
82 |
-
conn.close()
|
83 |
-
if not user_row:
|
84 |
return jsonify({"success": False, "error": "User not found"}), 401
|
85 |
-
user_id = user_row[0]
|
86 |
-
|
87 |
-
# Create initial document entry
|
88 |
doc_id = save_document(
|
89 |
title=filename,
|
90 |
-
full_text="",
|
91 |
summary="Processing...",
|
92 |
clauses="[]",
|
93 |
features="{}",
|
@@ -95,89 +90,44 @@ def upload_file():
|
|
95 |
file_path=file_path,
|
96 |
user_id=user_id
|
97 |
)
|
98 |
-
|
99 |
-
# Return immediate response with document ID
|
100 |
return jsonify({
|
101 |
'message': 'File uploaded successfully',
|
102 |
'document_id': doc_id,
|
103 |
'title': filename,
|
104 |
'status': 'processing'
|
105 |
}), 200
|
106 |
-
|
107 |
except Exception as e:
|
108 |
logging.error(f"Error during file upload: {str(e)}")
|
109 |
return jsonify({'error': str(e)}), 500
|
110 |
|
111 |
-
|
112 |
@main.route('/documents', methods=['GET'])
|
113 |
@jwt_required()
|
114 |
def list_documents():
|
115 |
-
logging.debug("Attempting to list documents...")
|
116 |
try:
|
117 |
-
identity = get_jwt_identity()
|
118 |
-
logging.debug(f"JWT identity for listing documents: {identity}")
|
119 |
docs = get_all_documents()
|
120 |
-
logging.info(f"Successfully fetched {len(docs)} documents.")
|
121 |
return jsonify(docs), 200
|
122 |
-
except jwt_exceptions.NoAuthorizationError as e:
|
123 |
-
logging.error(f"No authorization token provided for list documents: {str(e)}")
|
124 |
-
return jsonify({"success": False, "error": "Authorization token missing"}), 401
|
125 |
-
except jwt_exceptions.InvalidHeaderError as e:
|
126 |
-
logging.error(f"Invalid authorization header for list documents: {str(e)}")
|
127 |
-
return jsonify({"success": False, "error": "Invalid authorization header"}), 422
|
128 |
-
except JWTError as e: # Catch general JWT errors
|
129 |
-
logging.error(f"JWT error for list documents: {str(e)}")
|
130 |
-
return jsonify({"success": False, "error": f"JWT error: {str(e)}"}), 422
|
131 |
except Exception as e:
|
132 |
logging.error(f"Error listing documents: {str(e)}", exc_info=True)
|
133 |
return jsonify({"error": str(e)}), 500
|
134 |
|
135 |
-
|
136 |
@main.route('/get_document/<int:doc_id>', methods=['GET'])
|
137 |
@jwt_required()
|
138 |
def get_document(doc_id):
|
139 |
-
logging.debug(f"Attempting to get document with ID: {doc_id}")
|
140 |
try:
|
141 |
-
identity = get_jwt_identity()
|
142 |
-
logging.debug(f"JWT identity for getting document: {identity}")
|
143 |
doc = get_document_by_id(doc_id)
|
144 |
if doc:
|
145 |
-
logging.info(f"Successfully fetched document {doc_id}")
|
146 |
return jsonify(doc), 200
|
147 |
else:
|
148 |
-
logging.warning(f"Document with ID {doc_id} not found.")
|
149 |
return jsonify({"error": "Document not found"}), 404
|
150 |
-
except jwt_exceptions.NoAuthorizationError as e:
|
151 |
-
logging.error(f"No authorization token provided for get document: {str(e)}")
|
152 |
-
return jsonify({"success": False, "error": "Authorization token missing"}), 401
|
153 |
-
except jwt_exceptions.InvalidHeaderError as e:
|
154 |
-
logging.error(f"Invalid authorization header for get document: {str(e)}")
|
155 |
-
return jsonify({"success": False, "error": "Invalid authorization header"}), 422
|
156 |
-
except JWTError as e: # Catch general JWT errors
|
157 |
-
logging.error(f"JWT error for get document: {str(e)}")
|
158 |
-
return jsonify({"success": False, "error": f"JWT error: {str(e)}"}), 422
|
159 |
except Exception as e:
|
160 |
logging.error(f"Error getting document {doc_id}: {str(e)}", exc_info=True)
|
161 |
return jsonify({"error": str(e)}), 500
|
162 |
|
163 |
-
|
164 |
@main.route('/documents/download/<filename>', methods=['GET'])
|
165 |
@jwt_required()
|
166 |
def download_document(filename):
|
167 |
-
logging.debug(f"Attempting to download file: {filename}")
|
168 |
try:
|
169 |
-
identity = get_jwt_identity()
|
170 |
-
logging.debug(f"JWT identity for downloading document: {identity}")
|
171 |
return send_from_directory(UPLOAD_FOLDER, filename, as_attachment=True)
|
172 |
-
except jwt_exceptions.NoAuthorizationError as e:
|
173 |
-
logging.error(f"No authorization token provided for download document: {str(e)}")
|
174 |
-
return jsonify({"success": False, "error": "Authorization token missing"}), 401
|
175 |
-
except jwt_exceptions.InvalidHeaderError as e:
|
176 |
-
logging.error(f"Invalid authorization header for download document: {str(e)}")
|
177 |
-
return jsonify({"success": False, "error": "Invalid authorization header"}), 422
|
178 |
-
except JWTError as e: # Catch general JWT errors
|
179 |
-
logging.error(f"JWT error for download document: {str(e)}")
|
180 |
-
return jsonify({"success": False, "error": f"JWT error: {str(e)}"}), 422
|
181 |
except Exception as e:
|
182 |
logging.error(f"Error downloading file {filename}: {str(e)}", exc_info=True)
|
183 |
return jsonify({"error": f"Error downloading file: {str(e)}"}), 500
|
@@ -185,20 +135,8 @@ def download_document(filename):
|
|
185 |
@main.route('/documents/view/<filename>', methods=['GET'])
|
186 |
@jwt_required()
|
187 |
def view_document(filename):
|
188 |
-
logging.debug(f"Attempting to view file: {filename}")
|
189 |
try:
|
190 |
-
identity = get_jwt_identity()
|
191 |
-
logging.debug(f"JWT identity for viewing document: {identity}")
|
192 |
return send_from_directory(UPLOAD_FOLDER, filename)
|
193 |
-
except jwt_exceptions.NoAuthorizationError as e:
|
194 |
-
logging.error(f"No authorization token provided for view document: {str(e)}")
|
195 |
-
return jsonify({"success": False, "error": "Authorization token missing"}), 401
|
196 |
-
except jwt_exceptions.InvalidHeaderError as e:
|
197 |
-
logging.error(f"Invalid authorization header for view document: {str(e)}")
|
198 |
-
return jsonify({"success": False, "error": "Invalid authorization header"}), 422
|
199 |
-
except JWTError as e: # Catch general JWT errors
|
200 |
-
logging.error(f"JWT error for view document: {str(e)}")
|
201 |
-
return jsonify({"success": False, "error": f"JWT error: {str(e)}"}), 422
|
202 |
except Exception as e:
|
203 |
logging.error(f"Error viewing file {filename}: {str(e)}", exc_info=True)
|
204 |
return jsonify({"error": f"Error viewing file: {str(e)}"}), 500
|
@@ -206,30 +144,15 @@ def view_document(filename):
|
|
206 |
@main.route('/documents/<int:doc_id>', methods=['DELETE'])
|
207 |
@jwt_required()
|
208 |
def delete_document_route(doc_id):
|
209 |
-
logging.debug(f"Attempting to delete document with ID: {doc_id}")
|
210 |
try:
|
211 |
-
|
212 |
-
logging.debug(f"JWT identity for deleting document: {identity}")
|
213 |
-
file_path_to_delete = delete_document(doc_id) # This returns the file path
|
214 |
if file_path_to_delete and os.path.exists(file_path_to_delete):
|
215 |
os.remove(file_path_to_delete)
|
216 |
-
logging.info(f"Successfully deleted file {file_path_to_delete} from file system.")
|
217 |
-
logging.info(f"Document {doc_id} deleted from database.")
|
218 |
return jsonify({"success": True, "message": "Document deleted successfully"}), 200
|
219 |
-
except jwt_exceptions.NoAuthorizationError as e:
|
220 |
-
logging.error(f"No authorization token provided for delete document: {str(e)}")
|
221 |
-
return jsonify({"success": False, "error": "Authorization token missing"}), 401
|
222 |
-
except jwt_exceptions.InvalidHeaderError as e:
|
223 |
-
logging.error(f"Invalid authorization header for delete document: {str(e)}")
|
224 |
-
return jsonify({"success": False, "error": "Invalid authorization header"}), 422
|
225 |
-
except JWTError as e: # Catch general JWT errors
|
226 |
-
logging.error(f"JWT error for delete document: {str(e)}")
|
227 |
-
return jsonify({"success": False, "error": f"JWT error: {str(e)}"}), 422
|
228 |
except Exception as e:
|
229 |
logging.error(f"Error deleting document {doc_id}: {str(e)}", exc_info=True)
|
230 |
return jsonify({"success": False, "error": f"Error deleting document: {str(e)}"}), 500
|
231 |
|
232 |
-
|
233 |
@main.route('/register', methods=['POST'])
|
234 |
@handle_errors
|
235 |
def register():
|
@@ -237,32 +160,25 @@ def register():
|
|
237 |
username = data.get("username")
|
238 |
password = data.get("password")
|
239 |
email = data.get("email")
|
240 |
-
|
241 |
if not username or not password:
|
242 |
logging.warning("Registration attempt with missing username or password.")
|
243 |
return jsonify({"error": "Username and password are required"}), 400
|
244 |
-
|
245 |
hashed_pw = generate_password_hash(password)
|
246 |
-
|
247 |
-
|
248 |
try:
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
conn.commit()
|
253 |
-
logging.info(f"User {username} registered successfully.")
|
254 |
return jsonify({"message": "User registered successfully", "username": username, "email": email}), 201
|
255 |
-
except
|
256 |
-
|
257 |
return jsonify({"error": "Username already exists"}), 409
|
258 |
except Exception as e:
|
|
|
259 |
logging.error(f"Database error during registration: {str(e)}", exc_info=True)
|
260 |
return jsonify({"error": f"Database error: {str(e)}"}), 500
|
261 |
finally:
|
262 |
-
|
263 |
-
conn.close()
|
264 |
-
|
265 |
-
|
266 |
|
267 |
@main.route('/login', methods=['POST'])
|
268 |
@handle_errors
|
@@ -270,91 +186,60 @@ def login():
|
|
270 |
data = request.get_json()
|
271 |
username = data.get("username")
|
272 |
password = data.get("password")
|
273 |
-
|
274 |
if not username or not password:
|
275 |
logging.warning("Login attempt with missing username or password.")
|
276 |
return jsonify({"error": "Username and password are required"}), 400
|
277 |
-
|
278 |
-
conn = None
|
279 |
try:
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
"SELECT password_hash, email, username FROM users WHERE username = ? OR email = ?",
|
285 |
-
(username, username)
|
286 |
-
)
|
287 |
-
user = cursor.fetchone()
|
288 |
-
conn.close()
|
289 |
-
|
290 |
-
logging.debug(f"Login attempt for user: {username}")
|
291 |
-
if user:
|
292 |
-
stored_password_hash = user[0]
|
293 |
-
user_email = user[1]
|
294 |
-
user_username = user[2]
|
295 |
-
password_match = check_password_hash(stored_password_hash, password)
|
296 |
-
if password_match:
|
297 |
-
access_token = create_access_token(identity=user_username)
|
298 |
-
logging.info(f"User {user_username} logged in successfully.")
|
299 |
-
return jsonify(access_token=access_token, username=user_username, email=user_email), 200
|
300 |
-
else:
|
301 |
-
logging.warning(f"Failed login attempt for username/email: {username} - Incorrect password.")
|
302 |
-
return jsonify({"error": "Bad username or password"}), 401
|
303 |
else:
|
304 |
-
logging.warning(f"Failed login attempt: Username or email {username} not found.")
|
305 |
return jsonify({"error": "Bad username or password"}), 401
|
306 |
except Exception as e:
|
307 |
logging.error(f"Database error during login: {str(e)}", exc_info=True)
|
308 |
return jsonify({"error": f"Database error: {str(e)}"}), 500
|
309 |
finally:
|
310 |
-
|
311 |
-
conn.close()
|
312 |
-
|
313 |
|
314 |
@main.route('/process-document/<int:doc_id>', methods=['POST'])
|
315 |
@jwt_required()
|
316 |
def process_document(doc_id):
|
317 |
try:
|
318 |
-
# Get the document
|
319 |
document = get_document_by_id(doc_id)
|
320 |
if not document:
|
321 |
return jsonify({'error': 'Document not found'}), 404
|
322 |
-
|
323 |
file_path = document['file_path']
|
324 |
-
|
325 |
-
# Extract text
|
326 |
text = extract_text_from_file(file_path)
|
327 |
if not text:
|
328 |
return jsonify({'error': 'Could not extract text from file'}), 400
|
329 |
-
|
330 |
-
# Process the document
|
331 |
summary = generate_summary(text)
|
332 |
clauses = detect_clauses(text)
|
333 |
features = legal_domain_processor.process_legal_document(text)
|
334 |
context_analysis = context_processor.analyze_context(text)
|
335 |
-
|
336 |
# Update the document with processed content
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
|
|
|
|
347 |
return jsonify({
|
348 |
'message': 'Document processed successfully',
|
349 |
'document_id': doc_id,
|
350 |
'status': 'completed'
|
351 |
}), 200
|
352 |
-
|
353 |
except Exception as e:
|
354 |
logging.error(f"Error processing document: {str(e)}")
|
355 |
return jsonify({'error': str(e)}), 500
|
356 |
|
357 |
-
|
358 |
@main.route('/documents/summary/<int:doc_id>', methods=['POST'])
|
359 |
@jwt_required()
|
360 |
def generate_document_summary(doc_id):
|
@@ -362,24 +247,25 @@ def generate_document_summary(doc_id):
|
|
362 |
doc = get_document_by_id(doc_id)
|
363 |
if not doc:
|
364 |
return jsonify({"error": "Document not found"}), 404
|
365 |
-
# If summary exists and is not empty, return it
|
366 |
summary = doc.get('summary', '')
|
367 |
if summary and summary.strip() and summary != 'Processing...':
|
368 |
return jsonify({"summary": summary}), 200
|
369 |
file_path = doc.get('file_path', '')
|
370 |
if not file_path or not os.path.exists(file_path):
|
371 |
return jsonify({"error": "File not found for this document"}), 404
|
372 |
-
# Extract text from file (PDF, DOC, DOCX)
|
373 |
text = extract_text_from_file(file_path)
|
374 |
if not text.strip():
|
375 |
return jsonify({"error": "No text available for summarization"}), 400
|
376 |
summary = generate_summary(text)
|
377 |
# Save the summary to the database
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
|
|
|
|
|
|
383 |
return jsonify({"summary": summary}), 200
|
384 |
except Exception as e:
|
385 |
return jsonify({"error": f"Error generating summary: {str(e)}"}), 500
|
@@ -387,50 +273,29 @@ def generate_document_summary(doc_id):
|
|
387 |
@main.route('/ask-question', methods=['POST', 'OPTIONS'])
|
388 |
def ask_question():
|
389 |
if request.method == 'OPTIONS':
|
390 |
-
# Allow CORS preflight without authentication
|
391 |
return '', 204
|
392 |
return _ask_question_impl()
|
393 |
|
394 |
@jwt_required()
|
395 |
def _ask_question_impl():
|
396 |
-
logging.debug('ask_question route called. Method: %s', request.method)
|
397 |
data = request.get_json()
|
398 |
document_id = data.get('document_id')
|
399 |
question = data.get('question', '').strip()
|
400 |
if not document_id or not question:
|
401 |
-
logging.debug('Missing document_id or question in /ask-question')
|
402 |
return jsonify({"success": False, "error": "document_id and question are required"}), 400
|
403 |
if not question:
|
404 |
-
logging.debug('Empty question in /ask-question')
|
405 |
return jsonify({"success": False, "error": "Question cannot be empty"}), 400
|
406 |
identity = get_jwt_identity()
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
user_row = cursor.fetchone()
|
411 |
-
if not user_row:
|
412 |
-
conn.close()
|
413 |
-
logging.debug('User not found in /ask-question')
|
414 |
-
return jsonify({"success": False, "error": "User not found"}), 401
|
415 |
-
user_id = user_row[0]
|
416 |
-
# Fetch document and check ownership
|
417 |
-
cursor.execute('SELECT summary FROM documents WHERE id = ? AND user_id = ?', (document_id, user_id))
|
418 |
-
row = cursor.fetchone()
|
419 |
-
conn.close()
|
420 |
-
if not row:
|
421 |
-
logging.debug('Document not found or not owned by user in /ask-question')
|
422 |
return jsonify({"success": False, "error": "Document not found or not owned by user"}), 404
|
423 |
-
summary =
|
424 |
if not summary or not summary.strip():
|
425 |
-
logging.debug('Summary not available for this document in /ask-question')
|
426 |
return jsonify({"success": False, "error": "Summary not available for this document"}), 400
|
427 |
try:
|
428 |
result = answer_question(question, summary)
|
429 |
-
logging.debug('Answer generated successfully in /ask-question')
|
430 |
-
|
431 |
-
# Save the question and answer to database
|
432 |
save_question_answer(document_id, user_id, question, result.get('answer', ''), result.get('score', 0.0))
|
433 |
-
|
434 |
return jsonify({"success": True, "answer": result.get('answer', ''), "score": result.get('score', 0.0)}), 200
|
435 |
except Exception as e:
|
436 |
logging.error(f"Error answering question: {str(e)}")
|
@@ -441,62 +306,17 @@ def _ask_question_impl():
|
|
441 |
def get_previous_questions(doc_id):
|
442 |
try:
|
443 |
identity = get_jwt_identity()
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
user_row = cursor.fetchone()
|
448 |
-
if not user_row:
|
449 |
-
conn.close()
|
450 |
-
return jsonify({"success": False, "error": "User not found"}), 401
|
451 |
-
user_id = user_row[0]
|
452 |
-
|
453 |
-
# Check if document belongs to user
|
454 |
-
cursor.execute('SELECT id FROM documents WHERE id = ? AND user_id = ?', (doc_id, user_id))
|
455 |
-
if not cursor.fetchone():
|
456 |
-
conn.close()
|
457 |
return jsonify({"success": False, "error": "Document not found or not owned by user"}), 404
|
458 |
-
|
459 |
-
|
460 |
-
cursor.execute('''
|
461 |
-
SELECT id, question, answer, score, created_at
|
462 |
-
FROM question_answers
|
463 |
-
WHERE document_id = ? AND user_id = ?
|
464 |
-
ORDER BY created_at DESC
|
465 |
-
''', (doc_id, user_id))
|
466 |
-
|
467 |
-
questions = []
|
468 |
-
for row in cursor.fetchall():
|
469 |
-
questions.append({
|
470 |
-
'id': row[0],
|
471 |
-
'question': row[1],
|
472 |
-
'answer': row[2],
|
473 |
-
'score': row[3],
|
474 |
-
'timestamp': row[4]
|
475 |
-
})
|
476 |
-
|
477 |
-
conn.close()
|
478 |
return jsonify({"success": True, "questions": questions}), 200
|
479 |
-
|
480 |
except Exception as e:
|
481 |
logging.error(f"Error fetching previous questions: {str(e)}")
|
482 |
return jsonify({"success": False, "error": f"Error fetching previous questions: {str(e)}"}), 500
|
483 |
|
484 |
-
def save_question_answer(document_id, user_id, question, answer, score):
|
485 |
-
"""Save question and answer to database"""
|
486 |
-
try:
|
487 |
-
conn = sqlite3.connect(DB_PATH)
|
488 |
-
cursor = conn.cursor()
|
489 |
-
cursor.execute('''
|
490 |
-
INSERT INTO question_answers (document_id, user_id, question, answer, score, created_at)
|
491 |
-
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
492 |
-
''', (document_id, user_id, question, answer, score))
|
493 |
-
conn.commit()
|
494 |
-
conn.close()
|
495 |
-
logging.info(f"Question and answer saved for document {document_id}")
|
496 |
-
except Exception as e:
|
497 |
-
logging.error(f"Error saving question and answer: {str(e)}")
|
498 |
-
raise
|
499 |
-
|
500 |
@main.route('/search', methods=['GET'])
|
501 |
@jwt_required()
|
502 |
def search_all():
|
@@ -505,19 +325,8 @@ def search_all():
|
|
505 |
if not query:
|
506 |
return jsonify({'error': 'Query parameter "q" is required.'}), 400
|
507 |
identity = get_jwt_identity()
|
508 |
-
|
509 |
-
conn = sqlite3.connect(DB_PATH)
|
510 |
-
cursor = conn.cursor()
|
511 |
-
cursor.execute('SELECT id FROM users WHERE username = ?', (identity,))
|
512 |
-
user_row = cursor.fetchone()
|
513 |
-
conn.close()
|
514 |
-
if not user_row:
|
515 |
-
return jsonify({'error': 'User not found'}), 401
|
516 |
-
user_id = user_row[0]
|
517 |
-
# Search documents (title, summary)
|
518 |
-
from app.database import search_documents, search_questions_answers
|
519 |
doc_results = search_documents(query)
|
520 |
-
# Search Q&A
|
521 |
qa_results = search_questions_answers(query, user_id=user_id)
|
522 |
return jsonify({
|
523 |
'documents': doc_results,
|
@@ -575,34 +384,16 @@ def change_password():
|
|
575 |
def dashboard_stats():
|
576 |
try:
|
577 |
identity = get_jwt_identity()
|
578 |
-
|
579 |
-
conn = sqlite3.connect(DB_PATH)
|
580 |
-
cursor = conn.cursor()
|
581 |
-
cursor.execute('SELECT id FROM users WHERE username = ?', (identity,))
|
582 |
-
user_row = cursor.fetchone()
|
583 |
-
if not user_row:
|
584 |
-
conn.close()
|
585 |
-
return jsonify({'error': 'User not found'}), 401
|
586 |
-
user_id = user_row[0]
|
587 |
-
conn.close()
|
588 |
-
|
589 |
-
# Get all documents for this user
|
590 |
-
from app.database import get_all_documents
|
591 |
documents = get_all_documents(user_id=user_id)
|
592 |
total_documents = len(documents)
|
593 |
processed_documents = sum(1 for doc in documents if doc.get('summary') and doc.get('summary') != 'Processing...')
|
594 |
pending_analysis = total_documents - processed_documents
|
595 |
-
|
596 |
-
|
597 |
-
|
598 |
-
|
599 |
-
|
600 |
-
SELECT COUNT(*) FROM question_answers
|
601 |
-
WHERE user_id = ? AND created_at >= datetime('now', '-30 days')
|
602 |
-
''', (user_id,))
|
603 |
-
recent_questions = cursor.fetchone()[0]
|
604 |
-
conn.close()
|
605 |
-
|
606 |
return jsonify({
|
607 |
'total_documents': total_documents,
|
608 |
'processed_documents': processed_documents,
|
|
|
1 |
import os
|
|
|
2 |
from flask import Blueprint, request, jsonify, send_from_directory, current_app
|
3 |
from werkzeug.utils import secure_filename
|
4 |
from app.utils.extract_text import extract_text_from_pdf
|
|
|
6 |
from app.utils.clause_detector import detect_clauses
|
7 |
from app.database import save_document, delete_document
|
8 |
from app.database import get_all_documents, get_document_by_id
|
9 |
+
from app.database import search_documents, save_question_answer, search_questions_answers
|
10 |
from app.nlp.qa import answer_question
|
11 |
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, exceptions as jwt_exceptions
|
12 |
from flask_jwt_extended.exceptions import JWTDecodeError as JWTError
|
|
|
18 |
import logging
|
19 |
import textract
|
20 |
from app.database import get_user_profile, update_user_profile, change_user_password
|
21 |
+
from app.database import SessionLocal, User
|
22 |
+
from sqlalchemy.exc import IntegrityError
|
23 |
+
from sqlalchemy import or_
|
24 |
|
25 |
main = Blueprint("main", __name__)
|
26 |
|
|
|
30 |
context_processor = ContextUnderstanding()
|
31 |
|
32 |
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
|
33 |
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
|
34 |
|
35 |
# Ensure the upload folder exists
|
|
|
54 |
else:
|
55 |
raise Exception("Unsupported file type for text extraction.")
|
56 |
|
57 |
+
def get_user_id_by_username(username):
|
58 |
+
session = SessionLocal()
|
59 |
+
try:
|
60 |
+
user = session.query(User).filter(User.username == username).first()
|
61 |
+
return user.id if user else None
|
62 |
+
finally:
|
63 |
+
session.close()
|
64 |
+
|
65 |
@main.route('/upload', methods=['POST'])
|
66 |
@jwt_required()
|
67 |
def upload_file():
|
68 |
try:
|
69 |
if 'file' not in request.files:
|
70 |
return jsonify({'error': 'No file part'}), 400
|
|
|
71 |
file = request.files['file']
|
72 |
+
if not file or file.filename == '':
|
73 |
return jsonify({'error': 'No selected file'}), 400
|
|
|
|
|
74 |
if not (file.filename.lower().endswith('.pdf')):
|
75 |
return jsonify({'error': 'File type not allowed. Only PDF files are supported.'}), 400
|
|
|
|
|
76 |
filename = secure_filename(file.filename)
|
77 |
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
78 |
file.save(file_path)
|
|
|
|
|
79 |
identity = get_jwt_identity()
|
80 |
+
user_id = get_user_id_by_username(identity)
|
81 |
+
if not user_id:
|
|
|
|
|
|
|
|
|
82 |
return jsonify({"success": False, "error": "User not found"}), 401
|
|
|
|
|
|
|
83 |
doc_id = save_document(
|
84 |
title=filename,
|
85 |
+
full_text="",
|
86 |
summary="Processing...",
|
87 |
clauses="[]",
|
88 |
features="{}",
|
|
|
90 |
file_path=file_path,
|
91 |
user_id=user_id
|
92 |
)
|
|
|
|
|
93 |
return jsonify({
|
94 |
'message': 'File uploaded successfully',
|
95 |
'document_id': doc_id,
|
96 |
'title': filename,
|
97 |
'status': 'processing'
|
98 |
}), 200
|
|
|
99 |
except Exception as e:
|
100 |
logging.error(f"Error during file upload: {str(e)}")
|
101 |
return jsonify({'error': str(e)}), 500
|
102 |
|
|
|
103 |
@main.route('/documents', methods=['GET'])
|
104 |
@jwt_required()
|
105 |
def list_documents():
|
|
|
106 |
try:
|
|
|
|
|
107 |
docs = get_all_documents()
|
|
|
108 |
return jsonify(docs), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
except Exception as e:
|
110 |
logging.error(f"Error listing documents: {str(e)}", exc_info=True)
|
111 |
return jsonify({"error": str(e)}), 500
|
112 |
|
|
|
113 |
@main.route('/get_document/<int:doc_id>', methods=['GET'])
|
114 |
@jwt_required()
|
115 |
def get_document(doc_id):
|
|
|
116 |
try:
|
|
|
|
|
117 |
doc = get_document_by_id(doc_id)
|
118 |
if doc:
|
|
|
119 |
return jsonify(doc), 200
|
120 |
else:
|
|
|
121 |
return jsonify({"error": "Document not found"}), 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
except Exception as e:
|
123 |
logging.error(f"Error getting document {doc_id}: {str(e)}", exc_info=True)
|
124 |
return jsonify({"error": str(e)}), 500
|
125 |
|
|
|
126 |
@main.route('/documents/download/<filename>', methods=['GET'])
|
127 |
@jwt_required()
|
128 |
def download_document(filename):
|
|
|
129 |
try:
|
|
|
|
|
130 |
return send_from_directory(UPLOAD_FOLDER, filename, as_attachment=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
except Exception as e:
|
132 |
logging.error(f"Error downloading file {filename}: {str(e)}", exc_info=True)
|
133 |
return jsonify({"error": f"Error downloading file: {str(e)}"}), 500
|
|
|
135 |
@main.route('/documents/view/<filename>', methods=['GET'])
|
136 |
@jwt_required()
|
137 |
def view_document(filename):
|
|
|
138 |
try:
|
|
|
|
|
139 |
return send_from_directory(UPLOAD_FOLDER, filename)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
except Exception as e:
|
141 |
logging.error(f"Error viewing file {filename}: {str(e)}", exc_info=True)
|
142 |
return jsonify({"error": f"Error viewing file: {str(e)}"}), 500
|
|
|
144 |
@main.route('/documents/<int:doc_id>', methods=['DELETE'])
|
145 |
@jwt_required()
|
146 |
def delete_document_route(doc_id):
|
|
|
147 |
try:
|
148 |
+
file_path_to_delete = delete_document(doc_id)
|
|
|
|
|
149 |
if file_path_to_delete and os.path.exists(file_path_to_delete):
|
150 |
os.remove(file_path_to_delete)
|
|
|
|
|
151 |
return jsonify({"success": True, "message": "Document deleted successfully"}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
except Exception as e:
|
153 |
logging.error(f"Error deleting document {doc_id}: {str(e)}", exc_info=True)
|
154 |
return jsonify({"success": False, "error": f"Error deleting document: {str(e)}"}), 500
|
155 |
|
|
|
156 |
@main.route('/register', methods=['POST'])
|
157 |
@handle_errors
|
158 |
def register():
|
|
|
160 |
username = data.get("username")
|
161 |
password = data.get("password")
|
162 |
email = data.get("email")
|
|
|
163 |
if not username or not password:
|
164 |
logging.warning("Registration attempt with missing username or password.")
|
165 |
return jsonify({"error": "Username and password are required"}), 400
|
|
|
166 |
hashed_pw = generate_password_hash(password)
|
167 |
+
session = SessionLocal()
|
|
|
168 |
try:
|
169 |
+
user = User(username=username, password_hash=hashed_pw, email=email)
|
170 |
+
session.add(user)
|
171 |
+
session.commit()
|
|
|
|
|
172 |
return jsonify({"message": "User registered successfully", "username": username, "email": email}), 201
|
173 |
+
except IntegrityError:
|
174 |
+
session.rollback()
|
175 |
return jsonify({"error": "Username already exists"}), 409
|
176 |
except Exception as e:
|
177 |
+
session.rollback()
|
178 |
logging.error(f"Database error during registration: {str(e)}", exc_info=True)
|
179 |
return jsonify({"error": f"Database error: {str(e)}"}), 500
|
180 |
finally:
|
181 |
+
session.close()
|
|
|
|
|
|
|
182 |
|
183 |
@main.route('/login', methods=['POST'])
|
184 |
@handle_errors
|
|
|
186 |
data = request.get_json()
|
187 |
username = data.get("username")
|
188 |
password = data.get("password")
|
|
|
189 |
if not username or not password:
|
190 |
logging.warning("Login attempt with missing username or password.")
|
191 |
return jsonify({"error": "Username and password are required"}), 400
|
192 |
+
session = SessionLocal()
|
|
|
193 |
try:
|
194 |
+
user = session.query(User).filter(or_(User.username == username, User.email == username)).first()
|
195 |
+
if user and check_password_hash(user.password_hash, password):
|
196 |
+
access_token = create_access_token(identity=user.username)
|
197 |
+
return jsonify(access_token=access_token, username=user.username, email=user.email), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
198 |
else:
|
|
|
199 |
return jsonify({"error": "Bad username or password"}), 401
|
200 |
except Exception as e:
|
201 |
logging.error(f"Database error during login: {str(e)}", exc_info=True)
|
202 |
return jsonify({"error": f"Database error: {str(e)}"}), 500
|
203 |
finally:
|
204 |
+
session.close()
|
|
|
|
|
205 |
|
206 |
@main.route('/process-document/<int:doc_id>', methods=['POST'])
|
207 |
@jwt_required()
|
208 |
def process_document(doc_id):
|
209 |
try:
|
|
|
210 |
document = get_document_by_id(doc_id)
|
211 |
if not document:
|
212 |
return jsonify({'error': 'Document not found'}), 404
|
|
|
213 |
file_path = document['file_path']
|
|
|
|
|
214 |
text = extract_text_from_file(file_path)
|
215 |
if not text:
|
216 |
return jsonify({'error': 'Could not extract text from file'}), 400
|
|
|
|
|
217 |
summary = generate_summary(text)
|
218 |
clauses = detect_clauses(text)
|
219 |
features = legal_domain_processor.process_legal_document(text)
|
220 |
context_analysis = context_processor.analyze_context(text)
|
|
|
221 |
# Update the document with processed content
|
222 |
+
session = SessionLocal()
|
223 |
+
try:
|
224 |
+
doc = session.query(User).get(doc_id)
|
225 |
+
if doc:
|
226 |
+
doc.full_text = text
|
227 |
+
doc.summary = summary
|
228 |
+
doc.clauses = str(clauses)
|
229 |
+
doc.features = str(features)
|
230 |
+
doc.context_analysis = str(context_analysis)
|
231 |
+
session.commit()
|
232 |
+
finally:
|
233 |
+
session.close()
|
234 |
return jsonify({
|
235 |
'message': 'Document processed successfully',
|
236 |
'document_id': doc_id,
|
237 |
'status': 'completed'
|
238 |
}), 200
|
|
|
239 |
except Exception as e:
|
240 |
logging.error(f"Error processing document: {str(e)}")
|
241 |
return jsonify({'error': str(e)}), 500
|
242 |
|
|
|
243 |
@main.route('/documents/summary/<int:doc_id>', methods=['POST'])
|
244 |
@jwt_required()
|
245 |
def generate_document_summary(doc_id):
|
|
|
247 |
doc = get_document_by_id(doc_id)
|
248 |
if not doc:
|
249 |
return jsonify({"error": "Document not found"}), 404
|
|
|
250 |
summary = doc.get('summary', '')
|
251 |
if summary and summary.strip() and summary != 'Processing...':
|
252 |
return jsonify({"summary": summary}), 200
|
253 |
file_path = doc.get('file_path', '')
|
254 |
if not file_path or not os.path.exists(file_path):
|
255 |
return jsonify({"error": "File not found for this document"}), 404
|
|
|
256 |
text = extract_text_from_file(file_path)
|
257 |
if not text.strip():
|
258 |
return jsonify({"error": "No text available for summarization"}), 400
|
259 |
summary = generate_summary(text)
|
260 |
# Save the summary to the database
|
261 |
+
session = SessionLocal()
|
262 |
+
try:
|
263 |
+
document = session.query(User).get(doc_id)
|
264 |
+
if document:
|
265 |
+
document.summary = summary
|
266 |
+
session.commit()
|
267 |
+
finally:
|
268 |
+
session.close()
|
269 |
return jsonify({"summary": summary}), 200
|
270 |
except Exception as e:
|
271 |
return jsonify({"error": f"Error generating summary: {str(e)}"}), 500
|
|
|
273 |
@main.route('/ask-question', methods=['POST', 'OPTIONS'])
|
274 |
def ask_question():
|
275 |
if request.method == 'OPTIONS':
|
|
|
276 |
return '', 204
|
277 |
return _ask_question_impl()
|
278 |
|
279 |
@jwt_required()
|
280 |
def _ask_question_impl():
|
|
|
281 |
data = request.get_json()
|
282 |
document_id = data.get('document_id')
|
283 |
question = data.get('question', '').strip()
|
284 |
if not document_id or not question:
|
|
|
285 |
return jsonify({"success": False, "error": "document_id and question are required"}), 400
|
286 |
if not question:
|
|
|
287 |
return jsonify({"success": False, "error": "Question cannot be empty"}), 400
|
288 |
identity = get_jwt_identity()
|
289 |
+
user_id = get_user_id_by_username(identity)
|
290 |
+
doc = get_document_by_id(document_id, user_id=user_id)
|
291 |
+
if not doc:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
292 |
return jsonify({"success": False, "error": "Document not found or not owned by user"}), 404
|
293 |
+
summary = doc.get('summary', '')
|
294 |
if not summary or not summary.strip():
|
|
|
295 |
return jsonify({"success": False, "error": "Summary not available for this document"}), 400
|
296 |
try:
|
297 |
result = answer_question(question, summary)
|
|
|
|
|
|
|
298 |
save_question_answer(document_id, user_id, question, result.get('answer', ''), result.get('score', 0.0))
|
|
|
299 |
return jsonify({"success": True, "answer": result.get('answer', ''), "score": result.get('score', 0.0)}), 200
|
300 |
except Exception as e:
|
301 |
logging.error(f"Error answering question: {str(e)}")
|
|
|
306 |
def get_previous_questions(doc_id):
|
307 |
try:
|
308 |
identity = get_jwt_identity()
|
309 |
+
user_id = get_user_id_by_username(identity)
|
310 |
+
doc = get_document_by_id(doc_id, user_id=user_id)
|
311 |
+
if not doc:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
312 |
return jsonify({"success": False, "error": "Document not found or not owned by user"}), 404
|
313 |
+
qa_results = search_questions_answers('', user_id=user_id)
|
314 |
+
questions = [q for q in qa_results if q['document_id'] == doc_id]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
315 |
return jsonify({"success": True, "questions": questions}), 200
|
|
|
316 |
except Exception as e:
|
317 |
logging.error(f"Error fetching previous questions: {str(e)}")
|
318 |
return jsonify({"success": False, "error": f"Error fetching previous questions: {str(e)}"}), 500
|
319 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
320 |
@main.route('/search', methods=['GET'])
|
321 |
@jwt_required()
|
322 |
def search_all():
|
|
|
325 |
if not query:
|
326 |
return jsonify({'error': 'Query parameter "q" is required.'}), 400
|
327 |
identity = get_jwt_identity()
|
328 |
+
user_id = get_user_id_by_username(identity)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
329 |
doc_results = search_documents(query)
|
|
|
330 |
qa_results = search_questions_answers(query, user_id=user_id)
|
331 |
return jsonify({
|
332 |
'documents': doc_results,
|
|
|
384 |
def dashboard_stats():
|
385 |
try:
|
386 |
identity = get_jwt_identity()
|
387 |
+
user_id = get_user_id_by_username(identity)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
documents = get_all_documents(user_id=user_id)
|
389 |
total_documents = len(documents)
|
390 |
processed_documents = sum(1 for doc in documents if doc.get('summary') and doc.get('summary') != 'Processing...')
|
391 |
pending_analysis = total_documents - processed_documents
|
392 |
+
qa_results = search_questions_answers('', user_id=user_id)
|
393 |
+
from datetime import datetime, timedelta
|
394 |
+
now = datetime.utcnow()
|
395 |
+
last_30_days = now - timedelta(days=30)
|
396 |
+
recent_questions = sum(1 for q in qa_results if q['created_at'] and q['created_at'] >= last_30_days)
|
|
|
|
|
|
|
|
|
|
|
|
|
397 |
return jsonify({
|
398 |
'total_documents': total_documents,
|
399 |
'processed_documents': processed_documents,
|
backend/create_db.py
DELETED
@@ -1,17 +0,0 @@
|
|
1 |
-
import sqlite3
|
2 |
-
|
3 |
-
conn = sqlite3.connect('./legal_docs.db')
|
4 |
-
cursor = conn.cursor()
|
5 |
-
|
6 |
-
cursor.execute('''
|
7 |
-
CREATE TABLE IF NOT EXISTS users (
|
8 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
9 |
-
username TEXT UNIQUE NOT NULL,
|
10 |
-
password_hash TEXT NOT NULL,
|
11 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
12 |
-
)
|
13 |
-
''')
|
14 |
-
|
15 |
-
conn.commit()
|
16 |
-
conn.close()
|
17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/requirements.txt
CHANGED
Binary files a/backend/requirements.txt and b/backend/requirements.txt differ
|
|