gsingh78/ContractSense
0
1import os2from fastapi import FastAPI, HTTPException3from fastapi.responses import JSONResponse4from pydantic import BaseModel5from typing import List6import json7import csv8import io9import base6410import re11import logging12from langchain_openai import OpenAI13from langchain.chains import LLMChain14from langchain.prompts import PromptTemplate15from dotenv import load_dotenv16from docx import Document17import zipfile18import chardet19import pandas as pd20from fastapi.middleware.cors import CORSMiddleware21 22# Set up logging23logging.basicConfig(level=logging.DEBUG)24logger = logging.getLogger(__name__)25 26load_dotenv() # This loads the .env file27 28app = FastAPI()29 30# Add CORS middleware31app.add_middleware(32 CORSMiddleware,33 allow_origins=["*"], # Allows all origins34 allow_credentials=True,35 allow_methods=["*"], # Allows all methods36 allow_headers=["*"], # Allows all headers37)38 39class Task(BaseModel):40 description: str41 amount: float42 43class FileUpload(BaseModel):44 filename: str45 content: str46 47class TasksAnalysis(BaseModel):48 filename: str49 content: str50 contract_conditions: str51 52@app.post("/upload-contract")53async def upload_contract(file: FileUpload):54 logger.info(f"Received file: {file.filename}")55 if not file.filename.endswith('.docx'):56 logger.warning(f"Invalid file type: {file.filename}")57 raise HTTPException(status_code=400, detail="Invalid file type. Please upload a .docx file.")58 59 try:60 # Decode base64 content61 content = base64.b64decode(file.content)62 logger.info(f"Successfully decoded base64 content. Size: {len(content)} bytes")63 logger.info(f"First 20 bytes of content: {content[:20]}")64 65 # Attempt to open the content as a DOCX file66 try:67 contract_text = extract_text_from_docx(content)68 logger.info(f"Successfully extracted text from DOCX. Text length: {len(contract_text)}")69 except zipfile.BadZipFile:70 logger.error("The uploaded file is not a valid DOCX file.")71 raise HTTPException(status_code=400, detail="The uploaded file is not a valid DOCX file. Please ensure you're uploading a proper Microsoft Word document.")72 except Exception as docx_error:73 logger.error(f"Failed to read DOCX file: {str(docx_error)}")74 raise HTTPException(status_code=400, detail=f"Failed to read DOCX file: {str(docx_error)}")75 76 conditions = extract_contract_conditions(contract_text)77 logger.info("Successfully extracted contract conditions")78 return JSONResponse(content={"conditions": conditions})79 except base64.binascii.Error as be:80 logger.error(f"Invalid base64 encoding: {str(be)}")81 raise HTTPException(status_code=400, detail="Invalid file encoding. Please try uploading the file again.")82 except Exception as e:83 logger.error(f"An unexpected error occurred: {str(e)}", exc_info=True)84 raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")85 86@app.post("/analyze-tasks")87async def analyze_tasks(data: TasksAnalysis):88 try:89 # Decode base64 content90 content = base64.b64decode(data.content)91 tasks = parse_file(content, data.filename)92 results = analyze_task_compliance(tasks, json.loads(data.contract_conditions))93 return {"results": results}94 except base64.binascii.Error as be:95 logger.error(f"Invalid base64 encoding: {str(be)}")96 raise HTTPException(status_code=400, detail="Invalid file encoding. Please try uploading the file again.")97 except json.JSONDecodeError as je:98 logger.error(f"Invalid JSON in contract conditions: {str(je)}")99 raise HTTPException(status_code=400, detail="Invalid JSON in contract conditions")100 except Exception as e:101 logger.error(f"An unexpected error occurred: {str(e)}", exc_info=True)102 raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")103 104def extract_text_from_docx(content):105 doc = Document(io.BytesIO(content))106 return "\n".join([paragraph.text for paragraph in doc.paragraphs])107 108def extract_contract_conditions(content):109 llm = OpenAI(temperature=0)110 prompt = PromptTemplate(111 input_variables=["content"],112 template="""113 Extract key conditions from this contract, providing a more detailed structure:114 {content}115 Provide the output as a JSON string with the following structure:116 {{117 "travel_provisions": {{118 "budget_caps": {{119 "single_trip": float,120 "daily_expenses": float121 }},122 "multipliers": {{123 "night_weekend": float,124 "seasonal_location": float,125 "urgency": float126 }},127 "travel_class": {{128 "domestic": string,129 "international": {{130 "duration_threshold": int,131 "class": string132 }}133 }},134 "special_circumstances": {{135 "weather_allowance": float,136 "health_safety_covered": boolean137 }},138 "high_cost_locations": {{139 "increase_percentage": float,140 "approval_required": boolean141 }},142 "seasonal_adjustments": {{143 "increase_percentage": float144 }}145 }},146 "pre_approval_required": boolean,147 "expense_report_deadline": int148 }}149 """150 )151 chain = LLMChain(llm=llm, prompt=prompt)152 result = chain.run(content=content)153 154 logger.info(f"Raw API response: {result}")155 156 # Try to extract JSON from the response157 json_match = re.search(r'\{.*\}', result, re.DOTALL)158 if json_match:159 json_str = json_match.group(0)160 logger.info(f"Extracted JSON string: {json_str}")161 else:162 logger.error("No JSON-like structure found in the API response")163 raise ValueError("The API response does not contain a valid JSON structure")164 165 try:166 parsed_result = json.loads(json_str)167 return parsed_result168 except json.JSONDecodeError as e:169 logger.error(f"Failed to parse JSON: {e}")170 logger.error(f"Problematic JSON string: {json_str}")171 raise ValueError("Failed to parse the extracted JSON structure.")172 173def parse_text_response(response, original_amount):174 compliant = "is compliant" in response.lower()175 176 if compliant:177 reason = "Task is compliant with contract conditions"178 else:179 reason_match = re.search(r'(?:The task is not compliant because|The reason for (?:non-)?compliance is) (.*?)(?:\.|$)', response, re.IGNORECASE | re.DOTALL)180 reason = reason_match.group(1) if reason_match else "Unable to extract reason"181 182 adjusted_amount_match = re.search(r'adjusted amount:?\s*\$?([\d,]+(?:\.\d+)?)', response, re.IGNORECASE)183 adjusted_amount = float(adjusted_amount_match.group(1).replace(',', '')) if adjusted_amount_match else original_amount184 185 multipliers = re.findall(r'(\w+)(?: multiplier| adjustment)(?:s?):?\s*([\d.]+)', response, re.IGNORECASE)186 applied_multipliers = [f"{m[0]}: {m[1]}" for m in multipliers if m[1] != '0']187 188 additional_notes = response.split('Additional notes:', 1)[-1].strip() if 'Additional notes:' in response else response189 190 return {191 "compliant": compliant,192 "reason": reason,193 "adjusted_amount": adjusted_amount,194 "applied_multipliers": applied_multipliers,195 "additional_notes": additional_notes196 }197 198def analyze_task_compliance(tasks, conditions):199 llm = OpenAI(temperature=0)200 prompt = PromptTemplate(201 input_variables=["task", "conditions"],202 template="""203 Analyze if this task complies with the contract conditions:204 Task: {task}205 Conditions: {conditions}206 Provide a detailed analysis, considering all relevant factors such as budget caps, multipliers, travel class, special circumstances, and location-based adjustments.207 Start your response with "The task is compliant" or "The task is not compliant", followed by the reason.208 Then, state the adjusted amount (if applicable) as "Adjusted amount: $X".209 List any applied multipliers as "Applied multipliers: X: Y, Z: W".210 Finally, provide any additional notes or explanations under "Additional notes:".211 """212 )213 chain = LLMChain(llm=llm, prompt=prompt)214 results = []215 for task in tasks:216 try:217 result = chain.run(task=json.dumps(task.dict()), conditions=json.dumps(conditions))218 logger.info(f"Raw API response for task analysis: {result}")219 220 parsed_result = parse_text_response(result, task.amount)221 results.append(parsed_result)222 except Exception as e:223 logger.error(f"Unexpected error during task analysis: {e}")224 results.append({225 "compliant": False,226 "reason": f"Error in analysis: Unexpected error occurred",227 "adjusted_amount": task.amount,228 "applied_multipliers": [],229 "additional_notes": f"Error details: {str(e)}\nRaw API response: {result}"230 })231 return results232 233 234def parse_file(content, filename):235 if filename.endswith('.csv'):236 return parse_csv(content)237 elif filename.endswith(('.xls', '.xlsx')):238 return parse_excel(content)239 else:240 raise ValueError(f"Unsupported file type: {filename}")241 242def parse_csv(content):243 # Detect the file encoding244 detected = chardet.detect(content)245 file_encoding = detected['encoding']246 logger.info(f"Detected file encoding: {file_encoding}")247 248 # Try to decode the content with the detected encoding249 try:250 decoded_content = content.decode(file_encoding)251 except UnicodeDecodeError:252 logger.warning(f"Failed to decode with {file_encoding}, falling back to 'latin-1'")253 decoded_content = content.decode('latin-1')254 255 # Use csv.Sniffer to detect the dialect256 dialect = csv.Sniffer().sniff(decoded_content)257 logger.info(f"Detected CSV dialect: {dialect}")258 259 csv_reader = csv.DictReader(io.StringIO(decoded_content), dialect=dialect)260 return parse_rows(csv_reader)261 262def parse_excel(content):263 df = pd.read_excel(io.BytesIO(content))264 return parse_rows(df.to_dict('records'))265 266def parse_rows(rows):267 tasks = []268 for row in rows:269 try:270 amount = float(str(row['Amount']).replace('$', '').replace(',', ''))271 tasks.append(Task(description=row['Task Description'], amount=amount))272 except KeyError as e:273 logger.error(f"Missing required column: {e}")274 raise ValueError(f"File is missing required column: {e}")275 except ValueError as e:276 logger.error(f"Invalid amount format: {e}")277 raise ValueError(f"Invalid amount format in file: {e}")278 return tasks279 280if __name__ == "__main__":281 import uvicorn282 port = int(os.getenv("PORT", 8000))283 uvicorn.run(app, host="0.0.0.0", port=port)