Spaces:
Sleeping
Sleeping
Delete api
Browse files- api/__init__.py +0 -1
- api/api.py +0 -498
api/__init__.py
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
# API Package
|
|
|
|
api/api.py
DELETED
@@ -1,498 +0,0 @@
|
|
1 |
-
from fastapi import FastAPI, HTTPException, Depends, Query
|
2 |
-
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
3 |
-
from pydantic import BaseModel, HttpUrl
|
4 |
-
from typing import List, Dict, Any, Optional
|
5 |
-
import tempfile
|
6 |
-
import os
|
7 |
-
import hashlib
|
8 |
-
import asyncio
|
9 |
-
import aiohttp
|
10 |
-
import time
|
11 |
-
from contextlib import asynccontextmanager
|
12 |
-
|
13 |
-
from RAG.advanced_rag_processor import AdvancedRAGProcessor
|
14 |
-
from preprocessing.preprocessing import DocumentPreprocessor
|
15 |
-
from logger.logger import rag_logger
|
16 |
-
from LLM.llm_handler import llm_handler
|
17 |
-
from LLM.tabular_answer import get_answer_for_tabluar
|
18 |
-
from LLM.image_answerer import get_answer_for_image
|
19 |
-
from LLM.one_shotter import get_oneshot_answer
|
20 |
-
from config.config import *
|
21 |
-
import config.config as config
|
22 |
-
|
23 |
-
# Initialize security
|
24 |
-
security = HTTPBearer()
|
25 |
-
admin_security = HTTPBearer()
|
26 |
-
|
27 |
-
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
28 |
-
"""Verify the bearer token for main API."""
|
29 |
-
if credentials.credentials != BEARER_TOKEN:
|
30 |
-
raise HTTPException(
|
31 |
-
status_code=401,
|
32 |
-
detail="Invalid authentication token"
|
33 |
-
)
|
34 |
-
return credentials.credentials
|
35 |
-
|
36 |
-
def verify_admin_token(credentials: HTTPAuthorizationCredentials = Depends(admin_security)):
|
37 |
-
"""Verify the bearer token for admin endpoints."""
|
38 |
-
if credentials.credentials != "9420689497":
|
39 |
-
raise HTTPException(
|
40 |
-
status_code=401,
|
41 |
-
detail="Invalid admin authentication token"
|
42 |
-
)
|
43 |
-
return credentials.credentials
|
44 |
-
|
45 |
-
# Pydantic models for request/response
|
46 |
-
class ProcessDocumentRequest(BaseModel):
|
47 |
-
documents: HttpUrl # URL to the PDF document
|
48 |
-
questions: List[str] # List of questions to answer
|
49 |
-
|
50 |
-
class ProcessDocumentResponse(BaseModel):
|
51 |
-
answers: List[str]
|
52 |
-
|
53 |
-
class HealthResponse(BaseModel):
|
54 |
-
status: str
|
55 |
-
message: str
|
56 |
-
|
57 |
-
class PreprocessingResponse(BaseModel):
|
58 |
-
status: str
|
59 |
-
message: str
|
60 |
-
doc_id: str
|
61 |
-
chunk_count: int
|
62 |
-
|
63 |
-
class LogsResponse(BaseModel):
|
64 |
-
export_timestamp: str
|
65 |
-
metadata: Dict[str, Any]
|
66 |
-
logs: List[Dict[str, Any]]
|
67 |
-
|
68 |
-
class LogsSummaryResponse(BaseModel):
|
69 |
-
summary: Dict[str, Any]
|
70 |
-
|
71 |
-
# Global instances
|
72 |
-
rag_processor: Optional[AdvancedRAGProcessor] = None
|
73 |
-
document_preprocessor: Optional[DocumentPreprocessor] = None
|
74 |
-
|
75 |
-
@asynccontextmanager
|
76 |
-
async def lifespan(app: FastAPI):
|
77 |
-
"""Initialize and cleanup the RAG processor."""
|
78 |
-
global rag_processor, document_preprocessor
|
79 |
-
|
80 |
-
# Startup
|
81 |
-
print("🚀 Initializing Advanced RAG System...")
|
82 |
-
rag_processor = AdvancedRAGProcessor() # Use advanced processor for better accuracy
|
83 |
-
document_preprocessor = DocumentPreprocessor()
|
84 |
-
print("✅ Advanced RAG System initialized successfully")
|
85 |
-
|
86 |
-
yield
|
87 |
-
|
88 |
-
# Shutdown
|
89 |
-
print("🔄 Shutting down RAG System...")
|
90 |
-
if rag_processor:
|
91 |
-
rag_processor.cleanup()
|
92 |
-
print("✅ Cleanup completed")
|
93 |
-
|
94 |
-
# FastAPI app with lifespan management
|
95 |
-
app = FastAPI(
|
96 |
-
title="Advanced RAG API",
|
97 |
-
description="API for document processing and question answering using RAG",
|
98 |
-
version="1.0.0",
|
99 |
-
lifespan=lifespan
|
100 |
-
)
|
101 |
-
|
102 |
-
@app.get("/health", response_model=HealthResponse)
|
103 |
-
async def health_check():
|
104 |
-
"""Health check endpoint."""
|
105 |
-
return HealthResponse(
|
106 |
-
status="healthy",
|
107 |
-
message="RAG API is running successfully"
|
108 |
-
)
|
109 |
-
|
110 |
-
@app.post("/hackrx/run", response_model=ProcessDocumentResponse)
|
111 |
-
async def process_document(
|
112 |
-
request: ProcessDocumentRequest,
|
113 |
-
token: str = Depends(verify_token)
|
114 |
-
):
|
115 |
-
"""
|
116 |
-
Process a PDF document and answer questions about it.
|
117 |
-
|
118 |
-
This endpoint implements an optimized flow:
|
119 |
-
1. Check if the document is already processed (pre-computed embeddings)
|
120 |
-
2. If yes, use existing embeddings for fast retrieval + generation
|
121 |
-
3. If no, run full RAG pipeline (download + process + embed + store + answer)
|
122 |
-
|
123 |
-
Args:
|
124 |
-
request: Contains document URL and list of questions
|
125 |
-
token: Bearer token for authentication
|
126 |
-
|
127 |
-
Returns:
|
128 |
-
ProcessDocumentResponse: List of answers corresponding to the questions
|
129 |
-
"""
|
130 |
-
global rag_processor, document_preprocessor
|
131 |
-
|
132 |
-
if not rag_processor or not document_preprocessor:
|
133 |
-
raise HTTPException(
|
134 |
-
status_code=503,
|
135 |
-
detail="RAG system not initialized"
|
136 |
-
)
|
137 |
-
|
138 |
-
# Start timing and logging
|
139 |
-
start_time = time.time()
|
140 |
-
document_url = str(request.documents)
|
141 |
-
questions = request.questions
|
142 |
-
# Initialize answers for safe logging in finally
|
143 |
-
final_answers = []
|
144 |
-
status = "success"
|
145 |
-
error_message = None
|
146 |
-
doc_id = None
|
147 |
-
was_preprocessed = False
|
148 |
-
|
149 |
-
# Initialize enhanced logging
|
150 |
-
request_id = rag_logger.generate_request_id()
|
151 |
-
rag_logger.start_request_timing(request_id)
|
152 |
-
|
153 |
-
try:
|
154 |
-
print(f"📋 [{request_id}] Processing document: {document_url[:50]}...")
|
155 |
-
print(f"🤔 [{request_id}] Number of questions: {len(questions)}")
|
156 |
-
print(f"")
|
157 |
-
print(f"🚀 [{request_id}] ===== STARTING RAG PIPELINE =====")
|
158 |
-
print(f"📌 [{request_id}] PRIORITY 1: Checking stored embeddings database...")
|
159 |
-
|
160 |
-
# Generate document ID
|
161 |
-
doc_id = document_preprocessor.generate_doc_id(document_url)
|
162 |
-
|
163 |
-
# Step 1: Check if document is already processed (stored embeddings)
|
164 |
-
is_processed = document_preprocessor.is_document_processed(document_url)
|
165 |
-
was_preprocessed = is_processed
|
166 |
-
|
167 |
-
if is_processed:
|
168 |
-
print(f"✅ [{request_id}] ✅ FOUND STORED EMBEDDINGS for {doc_id}")
|
169 |
-
print(f"⚡ [{request_id}] Using fast path with pre-computed embeddings")
|
170 |
-
# Fast path: Use existing embeddings
|
171 |
-
doc_info = document_preprocessor.get_document_info(document_url)
|
172 |
-
print(f"📊 [{request_id}] Using existing collection with {doc_info.get('chunk_count', 'N/A')} chunks")
|
173 |
-
else:
|
174 |
-
print(f"❌ [{request_id}] No stored embeddings found for {doc_id}")
|
175 |
-
print(f"📌 [{request_id}] PRIORITY 2: Running full RAG pipeline (download + process + embed)...")
|
176 |
-
# Full path: Download and process document
|
177 |
-
resp = await document_preprocessor.process_document(document_url)
|
178 |
-
|
179 |
-
# Handle different return formats: [content, type] or [content, type, no_cleanup_flag]
|
180 |
-
if isinstance(resp, list):
|
181 |
-
content, _type = resp[0], resp[1]
|
182 |
-
if content == 'unsupported':
|
183 |
-
# Unsupported file type: respond gracefully without throwing server error
|
184 |
-
msg = f"Unsupported file type: {_type.lstrip('.')}"
|
185 |
-
final_answers = [msg]
|
186 |
-
status = "success" # ensure no 500 is raised in the finally block
|
187 |
-
return ProcessDocumentResponse(answers=final_answers)
|
188 |
-
|
189 |
-
if _type == "image":
|
190 |
-
try:
|
191 |
-
final_answers = get_answer_for_image(content, questions)
|
192 |
-
status = "success"
|
193 |
-
return ProcessDocumentResponse(answers=final_answers)
|
194 |
-
finally:
|
195 |
-
# Clean up the image file after processing
|
196 |
-
if os.path.exists(content):
|
197 |
-
os.unlink(content)
|
198 |
-
print(f"🗑️ Cleaned up image file: {content}")
|
199 |
-
|
200 |
-
if _type == "tabular":
|
201 |
-
final_answers = get_answer_for_tabluar(content, questions)
|
202 |
-
status = "success"
|
203 |
-
return ProcessDocumentResponse(answers=final_answers)
|
204 |
-
|
205 |
-
if _type == "oneshot":
|
206 |
-
# Process questions in batches for oneshot
|
207 |
-
tasks = [
|
208 |
-
get_oneshot_answer(content, questions[i:i + 3])
|
209 |
-
for i in range(0, len(questions), 3)
|
210 |
-
]
|
211 |
-
|
212 |
-
# Run all batches in parallel
|
213 |
-
results = await asyncio.gather(*tasks)
|
214 |
-
|
215 |
-
# Flatten results
|
216 |
-
final_answers = [ans for batch in results for ans in batch]
|
217 |
-
status = "success"
|
218 |
-
return ProcessDocumentResponse(answers=final_answers)
|
219 |
-
else:
|
220 |
-
doc_id = resp
|
221 |
-
|
222 |
-
print(f"✅ [{request_id}] Document {doc_id} processed and stored")
|
223 |
-
|
224 |
-
# Answer all questions using parallel processing for better latency
|
225 |
-
print(f"🚀 [{request_id}] Processing {len(questions)} questions in parallel...")
|
226 |
-
|
227 |
-
async def answer_single_question(question: str, index: int) -> tuple[str, Dict[str, float]]:
|
228 |
-
"""Answer a single question with error handling and timing."""
|
229 |
-
try:
|
230 |
-
question_start = time.time()
|
231 |
-
print(f"❓ [{request_id}] Q{index+1}: {question[:50]}...")
|
232 |
-
|
233 |
-
answer, pipeline_timings = await rag_processor.answer_question(
|
234 |
-
question=question,
|
235 |
-
doc_id=doc_id,
|
236 |
-
logger=rag_logger,
|
237 |
-
request_id=request_id
|
238 |
-
)
|
239 |
-
|
240 |
-
question_time = time.time() - question_start
|
241 |
-
|
242 |
-
# Log question timing
|
243 |
-
rag_logger.log_question_timing(
|
244 |
-
request_id, index, question, answer, question_time, pipeline_timings
|
245 |
-
)
|
246 |
-
|
247 |
-
print(f"✅ [{request_id}] Q{index+1} completed in {question_time:.4f}s")
|
248 |
-
return answer, pipeline_timings
|
249 |
-
except Exception as e:
|
250 |
-
print(f"❌ [{request_id}] Q{index+1} Error: {str(e)}")
|
251 |
-
return f"I encountered an error while processing this question: {str(e)}", {}
|
252 |
-
|
253 |
-
|
254 |
-
# Process questions in parallel with controlled concurrency
|
255 |
-
semaphore = asyncio.Semaphore(3) # Reduced concurrency for better logging visibility
|
256 |
-
|
257 |
-
async def bounded_answer(question: str, index: int) -> tuple[str, Dict[str, float]]:
|
258 |
-
async with semaphore:
|
259 |
-
return await answer_single_question(question, index)
|
260 |
-
|
261 |
-
# Execute all questions concurrently
|
262 |
-
tasks = [
|
263 |
-
bounded_answer(question, i)
|
264 |
-
for i, question in enumerate(questions)
|
265 |
-
]
|
266 |
-
|
267 |
-
results = await asyncio.gather(*tasks, return_exceptions=True)
|
268 |
-
|
269 |
-
# Handle any exceptions in answers
|
270 |
-
final_answers = []
|
271 |
-
error_count = 0
|
272 |
-
for i, result in enumerate(results):
|
273 |
-
if isinstance(result, Exception):
|
274 |
-
error_count += 1
|
275 |
-
final_answers.append(f"Error processing question {i+1}: {str(result)}")
|
276 |
-
else:
|
277 |
-
answer, _ = result
|
278 |
-
final_answers.append(answer)
|
279 |
-
|
280 |
-
# Determine final status
|
281 |
-
if error_count == 0:
|
282 |
-
status = "success"
|
283 |
-
elif error_count == len(questions):
|
284 |
-
status = "error"
|
285 |
-
else:
|
286 |
-
status = "partial"
|
287 |
-
|
288 |
-
print(f"✅ [{request_id}] Successfully processed {len(questions) - error_count}/{len(questions)} questions")
|
289 |
-
|
290 |
-
except Exception as e:
|
291 |
-
print(f"❌ [{request_id}] Error processing request: {str(e)}")
|
292 |
-
status = "error"
|
293 |
-
error_message = str(e)
|
294 |
-
final_answers = [f"Error: {str(e)}" for _ in questions]
|
295 |
-
|
296 |
-
finally:
|
297 |
-
# End request timing and get detailed timing data
|
298 |
-
timing_data = rag_logger.end_request_timing(request_id)
|
299 |
-
|
300 |
-
# Log the request with enhanced timing
|
301 |
-
processing_time = time.time() - start_time
|
302 |
-
logged_request_id = rag_logger.log_request(
|
303 |
-
document_url=document_url,
|
304 |
-
questions=questions,
|
305 |
-
answers=final_answers,
|
306 |
-
processing_time=processing_time,
|
307 |
-
status=status,
|
308 |
-
error_message=error_message,
|
309 |
-
document_id=doc_id,
|
310 |
-
was_preprocessed=was_preprocessed,
|
311 |
-
timing_data=timing_data
|
312 |
-
)
|
313 |
-
|
314 |
-
print(f"📊 Request logged with ID: {logged_request_id} (Status: {status}, Time: {processing_time:.2f}s)")
|
315 |
-
|
316 |
-
if status == "error":
|
317 |
-
raise HTTPException(
|
318 |
-
status_code=500,
|
319 |
-
detail=f"Failed to process document: {error_message}"
|
320 |
-
)
|
321 |
-
|
322 |
-
return ProcessDocumentResponse(answers=final_answers)
|
323 |
-
|
324 |
-
@app.post("/preprocess", response_model=PreprocessingResponse)
|
325 |
-
async def preprocess_document(document_url: str, force: bool = False, token: str = Depends(verify_admin_token)):
|
326 |
-
"""
|
327 |
-
Preprocess a document (for batch preprocessing).
|
328 |
-
|
329 |
-
Args:
|
330 |
-
document_url: URL of the PDF to preprocess
|
331 |
-
force: Whether to reprocess if already processed
|
332 |
-
|
333 |
-
Returns:
|
334 |
-
PreprocessingResponse: Status and document info
|
335 |
-
"""
|
336 |
-
global document_preprocessor
|
337 |
-
|
338 |
-
if not document_preprocessor:
|
339 |
-
raise HTTPException(
|
340 |
-
status_code=503,
|
341 |
-
detail="Document preprocessor not initialized"
|
342 |
-
)
|
343 |
-
|
344 |
-
try:
|
345 |
-
doc_id = await document_preprocessor.process_document(document_url, force)
|
346 |
-
doc_info = document_preprocessor.get_document_info(document_url)
|
347 |
-
|
348 |
-
return PreprocessingResponse(
|
349 |
-
status="success",
|
350 |
-
message=f"Document processed successfully",
|
351 |
-
doc_id=doc_id,
|
352 |
-
chunk_count=doc_info.get("chunk_count", 0)
|
353 |
-
)
|
354 |
-
|
355 |
-
except Exception as e:
|
356 |
-
raise HTTPException(
|
357 |
-
status_code=500,
|
358 |
-
detail=f"Failed to preprocess document: {str(e)}"
|
359 |
-
)
|
360 |
-
|
361 |
-
@app.get("/collections")
|
362 |
-
async def list_collections(token: str = Depends(verify_admin_token)):
|
363 |
-
"""List all available document collections."""
|
364 |
-
global document_preprocessor
|
365 |
-
|
366 |
-
if not document_preprocessor:
|
367 |
-
raise HTTPException(
|
368 |
-
status_code=503,
|
369 |
-
detail="Document preprocessor not initialized"
|
370 |
-
)
|
371 |
-
|
372 |
-
try:
|
373 |
-
processed_docs = document_preprocessor.list_processed_documents()
|
374 |
-
return {"collections": processed_docs}
|
375 |
-
except Exception as e:
|
376 |
-
raise HTTPException(
|
377 |
-
status_code=500,
|
378 |
-
detail=f"Failed to list collections: {str(e)}"
|
379 |
-
)
|
380 |
-
|
381 |
-
@app.get("/collections/stats")
|
382 |
-
async def get_collection_stats(token: str = Depends(verify_admin_token)):
|
383 |
-
"""Get statistics about all collections."""
|
384 |
-
global document_preprocessor
|
385 |
-
|
386 |
-
if not document_preprocessor:
|
387 |
-
raise HTTPException(
|
388 |
-
status_code=503,
|
389 |
-
detail="Document preprocessor not initialized"
|
390 |
-
)
|
391 |
-
|
392 |
-
try:
|
393 |
-
stats = document_preprocessor.get_collection_stats()
|
394 |
-
return stats
|
395 |
-
except Exception as e:
|
396 |
-
raise HTTPException(
|
397 |
-
status_code=500,
|
398 |
-
detail=f"Failed to get collection stats: {str(e)}"
|
399 |
-
)
|
400 |
-
|
401 |
-
# Logging Endpoints
|
402 |
-
@app.get("/logs", response_model=LogsResponse)
|
403 |
-
async def get_logs(
|
404 |
-
token: str = Depends(verify_admin_token),
|
405 |
-
limit: Optional[int] = Query(None, description="Maximum number of logs to return"),
|
406 |
-
minutes: Optional[int] = Query(None, description="Get logs from last N minutes"),
|
407 |
-
document_url: Optional[str] = Query(None, description="Filter logs by document URL")
|
408 |
-
):
|
409 |
-
"""
|
410 |
-
Export all API request logs as JSON.
|
411 |
-
|
412 |
-
Query Parameters:
|
413 |
-
limit: Maximum number of recent logs to return
|
414 |
-
minutes: Get logs from the last N minutes
|
415 |
-
document_url: Filter logs for a specific document URL
|
416 |
-
|
417 |
-
Returns:
|
418 |
-
LogsResponse: Complete logs export with metadata
|
419 |
-
"""
|
420 |
-
try:
|
421 |
-
if document_url:
|
422 |
-
# Get logs for specific document
|
423 |
-
logs = rag_logger.get_logs_by_document(document_url)
|
424 |
-
metadata = {
|
425 |
-
"filtered_by": "document_url",
|
426 |
-
"document_url": document_url,
|
427 |
-
"total_logs": len(logs)
|
428 |
-
}
|
429 |
-
return LogsResponse(
|
430 |
-
export_timestamp=rag_logger.export_logs()["export_timestamp"],
|
431 |
-
metadata=metadata,
|
432 |
-
logs=logs
|
433 |
-
)
|
434 |
-
|
435 |
-
elif minutes:
|
436 |
-
# Get recent logs
|
437 |
-
logs = rag_logger.get_recent_logs(minutes)
|
438 |
-
metadata = {
|
439 |
-
"filtered_by": "time_range",
|
440 |
-
"minutes": minutes,
|
441 |
-
"total_logs": len(logs)
|
442 |
-
}
|
443 |
-
return LogsResponse(
|
444 |
-
export_timestamp=rag_logger.export_logs()["export_timestamp"],
|
445 |
-
metadata=metadata,
|
446 |
-
logs=logs
|
447 |
-
)
|
448 |
-
|
449 |
-
else:
|
450 |
-
# Get all logs (with optional limit)
|
451 |
-
if limit:
|
452 |
-
logs = rag_logger.get_logs(limit)
|
453 |
-
metadata = rag_logger.get_logs_summary()
|
454 |
-
metadata["limited_to"] = limit
|
455 |
-
else:
|
456 |
-
logs_export = rag_logger.export_logs()
|
457 |
-
return LogsResponse(**logs_export)
|
458 |
-
|
459 |
-
return LogsResponse(
|
460 |
-
export_timestamp=rag_logger.export_logs()["export_timestamp"],
|
461 |
-
metadata=metadata,
|
462 |
-
logs=logs
|
463 |
-
)
|
464 |
-
|
465 |
-
except Exception as e:
|
466 |
-
raise HTTPException(
|
467 |
-
status_code=500,
|
468 |
-
detail=f"Failed to export logs: {str(e)}"
|
469 |
-
)
|
470 |
-
|
471 |
-
@app.get("/logs/summary", response_model=LogsSummaryResponse)
|
472 |
-
async def get_logs_summary(token: str = Depends(verify_admin_token)):
|
473 |
-
"""
|
474 |
-
Get summary statistics of all logs.
|
475 |
-
|
476 |
-
Returns:
|
477 |
-
LogsSummaryResponse: Summary statistics
|
478 |
-
"""
|
479 |
-
try:
|
480 |
-
summary = rag_logger.get_logs_summary()
|
481 |
-
return LogsSummaryResponse(summary=summary)
|
482 |
-
except Exception as e:
|
483 |
-
raise HTTPException(
|
484 |
-
status_code=500,
|
485 |
-
detail=f"Failed to get logs summary: {str(e)}"
|
486 |
-
)
|
487 |
-
|
488 |
-
if __name__ == "__main__":
|
489 |
-
import uvicorn
|
490 |
-
|
491 |
-
# Run the FastAPI server
|
492 |
-
uvicorn.run(
|
493 |
-
"api:app",
|
494 |
-
host=API_HOST,
|
495 |
-
port=API_PORT,
|
496 |
-
reload=API_RELOAD,
|
497 |
-
log_level="info"
|
498 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|