CoolFace
Apppublic

gsingh78/ContractSense

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
main.py283 linesDownload Raw Back to root
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)