| import os |
| import anthropic |
| import requests |
| import streamlit as st |
| import numpy as np |
| import json |
| import re |
| from requests.adapters import HTTPAdapter |
| from urllib3.util.retry import Retry |
| from src.extract_text.google_document_api import GoogleDocumentAPI |
|
|
| CLAUDE_API_URL = "https://api.anthropic.com/v1/messages" |
|
|
|
|
|
|
| class LLM: |
| def __init__(self): |
| self.claude_api_key = os.getenv('CLAUDE_API_KEY') |
| if not self.claude_api_key: |
| raise ValueError("Please set the CLAUDE_API_KEY environment variable.") |
| |
| |
| retry_strategy = Retry( |
| total=5, |
| backoff_factor=2, |
| status_forcelist=[429, 500, 502, 503, 504, 529], |
| allowed_methods=["POST"], |
| respect_retry_after_header=True, |
| ) |
| |
| |
| self.session = requests.Session() |
| self.session.mount("https://", HTTPAdapter(max_retries=retry_strategy)) |
|
|
| def call_claude_api(self, prompt, system_prompt, model="claude-sonnet-4-20250514", max_tokens=2000) -> str: |
| """ |
| Helper function to call Claude API with consistent parameters and enhanced error handling. |
| """ |
| headers = { |
| "x-api-key": self.claude_api_key, |
| "anthropic-version": "2023-06-01", |
| "Content-Type": "application/json" |
| } |
| |
| payload = { |
| "model": model, |
| "max_tokens": max_tokens, |
| "temperature": 0.1, |
| "messages": [ |
| { |
| "role": "user", |
| "content": prompt |
| } |
| ], |
| "system": system_prompt |
| } |
| |
| max_retries = 3 |
| for attempt in range(max_retries): |
| try: |
| response = self.session.post( |
| CLAUDE_API_URL, |
| headers=headers, |
| json=payload, |
| verify=True, |
| timeout=60 |
| ) |
| |
| |
| if response.status_code == 529: |
| st.warning(f"Server overload (529) on attempt {attempt + 1}/{max_retries}. Retrying with exponential backoff...") |
| if attempt < max_retries - 1: |
| import time |
| time.sleep(2 ** attempt) |
| continue |
| else: |
| st.error("Server overload after all retries. Please try again later.") |
| return "" |
| |
| response.raise_for_status() |
| |
| |
| response_data = response.json() |
| if "content" in response_data and len(response_data["content"]) > 0: |
| return response_data["content"][0]["text"] |
| else: |
| st.error("Unexpected response format from Claude API") |
| return "" |
| |
| except requests.exceptions.SSLError as ssl_err: |
| st.error(f"SSL Error when calling Claude API. Please check your SSL certificates and network connection. Error: {ssl_err}") |
| return "" |
| except requests.exceptions.Timeout as timeout_err: |
| st.warning(f"Timeout on attempt {attempt + 1}/{max_retries}. Retrying...") |
| if attempt == max_retries - 1: |
| st.error("Request timed out after all retries") |
| return "" |
| except requests.exceptions.RequestException as e: |
| st.error(f"Error calling Claude API: {str(e)}") |
| return "" |
| except json.JSONDecodeError as json_err: |
| st.error(f"Invalid JSON response from Claude API: {json_err}") |
| return "" |
| |
| return "" |
|
|
| def call_claude_vision_api(self, prompt, system_prompt, image_base64, model="claude-sonnet-4-20250514", max_tokens=2000) -> str: |
| """ |
| Helper function to call Claude Vision API with image support and enhanced error handling. |
| """ |
| headers = { |
| "x-api-key": self.claude_api_key, |
| "anthropic-version": "2023-06-01", |
| "Content-Type": "application/json" |
| } |
| |
| content = [ |
| { |
| "type": "text", |
| "text": prompt |
| }, |
| { |
| "type": "image", |
| "source": { |
| "type": "base64", |
| "media_type": "image/png", |
| "data": image_base64 |
| } |
| } |
| ] |
| |
| payload = { |
| "model": model, |
| "max_tokens": max_tokens, |
| "temperature": 0, |
| "messages": [ |
| { |
| "role": "user", |
| "content": content |
| } |
| ], |
| "system": system_prompt |
| } |
| |
| max_retries = 3 |
| for attempt in range(max_retries): |
| try: |
| response = self.session.post( |
| CLAUDE_API_URL, |
| headers=headers, |
| json=payload, |
| verify=True, |
| timeout=90 |
| ) |
| |
| |
| if response.status_code == 529: |
| st.warning(f"Server overload (529) on attempt {attempt + 1}/{max_retries}. Retrying with exponential backoff...") |
| if attempt < max_retries - 1: |
| import time |
| time.sleep(2 ** attempt) |
| continue |
| else: |
| st.error("Server overload after all retries. Please try again later.") |
| return "" |
| |
| response.raise_for_status() |
| |
| |
| response_data = response.json() |
| if "content" in response_data and len(response_data["content"]) > 0: |
| return response_data["content"][0]["text"] |
| else: |
| st.error("Unexpected response format from Claude Vision API") |
| return "" |
| |
| except requests.exceptions.SSLError as ssl_err: |
| st.error(f"SSL Error when calling Claude Vision API. Please check your SSL certificates and network connection. Error: {ssl_err}") |
| return "" |
| except requests.exceptions.Timeout as timeout_err: |
| st.warning(f"Timeout on attempt {attempt + 1}/{max_retries}. Retrying...") |
| if attempt == max_retries - 1: |
| st.error("Request timed out after all retries") |
| return "" |
| except requests.exceptions.RequestException as e: |
| st.error(f"Error calling Claude Vision API: {str(e)}") |
| return "" |
| except json.JSONDecodeError as json_err: |
| st.error(f"Invalid JSON response from Claude Vision API: {json_err}") |
| return "" |
| |
| return "" |
|
|
| def call_claude_pdf_api(self, prompt, system_prompt, pdf_base64, model="claude-sonnet-4-20250514", max_tokens=4000) -> str: |
| """ |
| Helper function to call Claude API with PDF support for requirements documents. |
| For now, we'll fall back to text-based processing since PDF API requires specific setup. |
| """ |
| |
| |
| st.info("π PDF requirements detected. Using text-based processing for now.") |
| st.info("π‘ For full visual PDF analysis, consider using the Converse API with citations enabled.") |
| |
| |
| |
| try: |
| import base64 |
| import io |
| |
| |
| try: |
| from PyPDF2 import PdfReader |
| pdf_reader_available = True |
| except ImportError: |
| pdf_reader_available = False |
| st.warning("PyPDF2 not available. Using basic text processing for PDF.") |
| |
| if pdf_reader_available: |
| |
| pdf_bytes = base64.b64decode(pdf_base64) |
| pdf_stream = io.BytesIO(pdf_bytes) |
| |
| |
| reader = PdfReader(pdf_stream) |
| text_content = "" |
| for page in reader.pages: |
| text_content += page.extract_text() + "\n" |
| |
| if not text_content.strip(): |
| text_content = "PDF Requirements Document (text extraction limited)" |
| |
| |
| return self.call_claude_api(prompt, system_prompt, model=model, max_tokens=max_tokens) |
| else: |
| |
| return self.call_claude_api(prompt, system_prompt, model=model, max_tokens=max_tokens) |
| |
| except Exception as e: |
| st.warning(f"PDF text extraction failed: {e}") |
| st.warning("Falling back to basic text processing") |
| |
| |
| return self.call_claude_api(prompt, system_prompt, model=model, max_tokens=max_tokens) |
|
|
| class ComplianceAnalysis: |
| def __init__(self): |
| self.llm = LLM() |
|
|
| def extract_structured_requirements(self, requirements_data) -> list[dict]: |
| """ |
| Use Claude to extract structured requirements from the requirements document. |
| |
| Args: |
| requirements_data: Either a string (for text files) or a dict (for PDF files) containing requirements. |
| |
| Returns: |
| A list of dictionaries, each containing a requirement ID, description, and category. |
| """ |
| |
| if isinstance(requirements_data, str): |
| |
| requirements_text = requirements_data |
| requirements_type = "text" |
| elif isinstance(requirements_data, dict): |
| |
| requirements_text = requirements_data.get('text_content', '') |
| requirements_type = requirements_data.get('type', 'text') |
| pdf_base64 = requirements_data.get('content', '') if requirements_type == 'pdf' else None |
| else: |
| st.error("Invalid requirements data format. Please upload a valid requirements document.") |
| return [] |
| |
| |
| if not requirements_text or not requirements_text.strip(): |
| st.error("Requirements text is empty. Please upload a valid requirements document.") |
| return [] |
| |
| system_prompt = """You are an expert requirements analyst. Extract clear, structured requirements from documents. You must always return valid JSON, even if no specific requirements are found.""" |
| |
| extraction_prompt = f""" |
| Extract all requirements from this document (not just allergen requirements): |
| |
| {requirements_text} |
| |
| For each requirement found, provide: |
| 1. Unique ID (REQ001, REQ002, etc.) |
| 2. Description (verbatim from the document) |
| 3. Category (Font Size, Allergen List, Formatting, Placement, Barcode, Organic, Promotional, etc.) |
| 4. Source reference (section/paragraph or line number) |
| |
| If no requirements are found, return an empty array: [] |
| |
| Return as JSON array with fields: id, description, category, source_reference. |
| |
| Example: |
| ```json |
| [ |
| {{ |
| "id": "REQ001", |
| "description": "IF the product is labeled as organic, THEN a certified organic seal must be visible", |
| "category": "Organic", |
| "source_reference": "Line 1" |
| }}, |
| {{ |
| "id": "REQ002", |
| "description": "IF there is a promotional offer mentioned, THEN include the offer expiry date", |
| "category": "Promotional", |
| "source_reference": "Line 2" |
| }} |
| ] |
| ``` |
| |
| IMPORTANT: Always return valid JSON. If you cannot extract any requirements, return an empty array: [] |
| """ |
| |
| |
| if requirements_type == 'pdf' and pdf_base64: |
| |
| response = self.llm.call_claude_pdf_api(extraction_prompt, system_prompt, pdf_base64, model='claude-sonnet-4-20250514') |
| else: |
| |
| response = self.llm.call_claude_api(extraction_prompt, system_prompt, model='claude-3-5-haiku-20241022') |
| |
| |
| try: |
| |
| if "```json" in response and "```" in response.split("```json")[1]: |
| json_content = response.split("```json")[1].split("```")[0].strip() |
| elif "```" in response: |
| |
| json_content = response.split("```")[1].split("```")[0].strip() |
| else: |
| |
| json_content = response |
| |
| |
| |
| json_content = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', json_content) |
| |
| json_content = re.sub(r'(?<!\\)"(?:[^"\\]|\\.)*?(?<!\\)"', lambda m: m.group(0).replace('\n', '\\n'), json_content) |
| |
| requirements = json.loads(json_content) |
| return requirements |
| except Exception as e: |
| st.error(f"Error parsing extracted requirements: {e}") |
| st.error(f"Raw response: {response}") |
| |
| return [] |
|
|
|
|
| def verify_individual_requirement(self, requirement, markdown_table, image=None, barcode_data=None, metadata=None, requirements_data=None): |
| """ |
| Use structured reasoning to verify if a specific requirement is met in the packaging text. |
| |
| Args: |
| requirement: A dictionary containing requirement details |
| markdown_table: The markdown table extracted from the packaging PDF |
| image: The image of the packaging document (optional) |
| barcode_data: List of barcode objects with position data (optional) |
| metadata: Dictionary containing font, font size, and color metadata (optional) |
| requirements_data: Original requirements data (text or PDF) for context (optional) |
| Returns: |
| A dictionary with verification results including reasoning and compliance status |
| """ |
| system_prompt = """You are a regulatory compliance expert. Provide detailed, objective compliance reports.""" |
| |
| |
| verification_prompt = f""" |
| You are a regulatory compliance expert. Provide detailed, objective compliance reports. |
| I need to verify if the following specific requirement is met in the packaging text: |
| |
| Requirement ID: {requirement['id']} |
| Requirement Description: {requirement['description']} |
| Requirement Category: {requirement['category']} |
| |
| Here is the packaging text to analyze: |
| |
| {markdown_table} |
| """ |
| |
| |
| if barcode_data: |
| |
| barcode_summary = [] |
| for barcode in barcode_data: |
| barcode_summary.append({ |
| 'id': barcode['id'], |
| 'type': barcode['type'], |
| 'data': barcode['data'], |
| 'valid': barcode['valid'] |
| }) |
| |
| verification_prompt += f""" |
| |
| Barcode Information Found: |
| {json.dumps(barcode_summary, indent=2)} |
| |
| When analyzing barcode-related requirements, consider: |
| - Barcode ID for evidence reference |
| - Barcode type and validation status |
| """ |
| |
| |
| if metadata and not metadata.get('error'): |
| |
| metadata_summary = { |
| 'extraction_method': metadata.get('extraction_method', 'unknown'), |
| 'has_selectable_text': metadata.get('has_selectable_text', False), |
| 'pages_processed': metadata.get('pages_processed', 0), |
| 'dominant_font': metadata.get('fonts', {}), |
| 'dominant_font_size': metadata.get('font_sizes', {}), |
| 'dominant_text_color': metadata.get('text_colors', {}) |
| } |
| |
| verification_prompt += f""" |
| |
| Typography and Design Metadata: |
| {json.dumps(metadata_summary, indent=2)} |
| |
| When analyzing typography and design requirements, consider: |
| - Font types and their usage frequency |
| - Font sizes and their distribution |
| - Text colors and their application |
| - Whether text is selectable or requires OCR |
| """ |
| |
| verification_prompt += f""" |
| |
| Verify this requirement using these steps: |
| 1. Break down into checkable criteria |
| 2. Search for evidence in packaging text (provide Text ID) |
| 3. For visual elements not in text, describe clearly (text_id = null) |
| 4. For barcode evidence, use Barcode ID (text_id = null) |
| 5. Provide specific examples/quotes |
| 6. Determine: COMPLIANT/NON-COMPLIANT/PARTIALLY COMPLIANT |
| - Compliant: All applicable rules are fully met without any deviation. |
| - Partially Compliant: Some rules are met, but minor issues/omissions that don't constitute a full failure but need attention. |
| - Non-Compliant: One or more critical rules are violated or omitted, posing a regulatory, safety, or logistical risk. |
| 7. Explain reasoning |
| |
| For visual evidence, describe: |
| - Location (e.g., "top right corner", "bottom section") |
| - Visual characteristics (e.g., "large bold text", "red warning box") |
| - Content description (e.g., "allergen warning in red box") |
| |
| If there is barcode evidence, include: |
| - Barcode ID |
| - Barcode type and validation status |
| |
| Return JSON with structure: |
| ```json |
| {{ |
| "requirement_id": "{requirement['id']}", |
| "criteria": ["criterion 1", "criterion 2"], |
| "evidence_found": [ |
| {{"text_id": <Text ID or null>, "evidence_text": "<description>", "barcode_id": "<Barcode ID ONLY if applicable>"}} |
| ], |
| "compliance_status": "COMPLIANT/NON-COMPLIANT/PARTIALLY COMPLIANT", |
| "reasoning": "Detailed explanation", |
| "confidence": 0.95 |
| }} |
| ``` |
| """ |
| |
| |
| if image: |
| response = self.llm.call_claude_vision_api(verification_prompt, system_prompt, image) |
| else: |
| response = self.llm.call_claude_api(verification_prompt, system_prompt) |
| |
| |
| try: |
| |
| if not response or not response.strip(): |
| st.error("Empty response received from Claude API") |
| return { |
| "requirement_id": requirement['id'], |
| "evidence_found": [], |
| "compliance_status": "ERROR", |
| "reasoning": "Empty response received from Claude API", |
| "confidence": 0 |
| } |
| |
| |
| if "```json" in response and "```" in response.split("```json")[1]: |
| json_content = response.split("```json")[1].split("```")[0].strip() |
| elif "```" in response: |
| |
| json_content = response.split("```")[1].split("```")[0].strip() |
| else: |
| |
| json_content = response |
| |
| |
| |
| json_content = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', json_content) |
| |
| json_content = re.sub(r'(?<!\\)"(?:[^"\\]|\\.)*?(?<!\\)"', lambda m: m.group(0).replace('\n', '\\n'), json_content) |
| |
| |
| verification_result = None |
| |
| |
| try: |
| verification_result = json.loads(json_content) |
| except json.JSONDecodeError as e1: |
| st.warning(f"Initial JSON parsing failed: {e1}") |
| |
| |
| try: |
| |
| json_match = re.search(r'\{.*\}', json_content, re.DOTALL) |
| if json_match: |
| potential_json = json_match.group(0) |
| verification_result = json.loads(potential_json) |
| st.info("Successfully extracted JSON from malformed response") |
| except json.JSONDecodeError as e2: |
| st.warning(f"JSON extraction failed: {e2}") |
| |
| |
| try: |
| |
| compliance_status = "UNKNOWN" |
| if "COMPLIANT" in response.upper(): |
| compliance_status = "COMPLIANT" |
| elif "NON-COMPLIANT" in response.upper(): |
| compliance_status = "NON-COMPLIANT" |
| elif "PARTIALLY" in response.upper(): |
| compliance_status = "PARTIALLY COMPLIANT" |
| |
| verification_result = { |
| "requirement_id": requirement['id'], |
| "criteria": ["Unable to parse criteria"], |
| "evidence_found": [], |
| "compliance_status": compliance_status, |
| "reasoning": f"Response parsing failed. Raw response: {response[:200]}...", |
| "confidence": 0.1 |
| } |
| st.warning("Created fallback JSON structure due to parsing errors") |
| except Exception as e3: |
| st.error(f"Fallback JSON creation failed: {e3}") |
| raise e3 |
| |
| if verification_result: |
| return verification_result |
| else: |
| raise Exception("All JSON parsing strategies failed") |
| |
| except Exception as e: |
| st.error(f"Error parsing verification result: {e}") |
| st.error(f"Raw response: {response}") |
| |
| return { |
| "requirement_id": requirement['id'], |
| "evidence_found": [], |
| "compliance_status": "ERROR", |
| "reasoning": f"Failed to verify requirement due to parsing error: {str(e)}", |
| "confidence": 0 |
| } |
|
|
|
|
|
|
| def analyze_compliance(self, requirements_data, packaging_text, packaging_data, image=None, barcode_data=None, metadata=None, model="claude-sonnet-4-20250514"): |
| """ |
| Analyze packaging compliance through multi-step process: |
| 1. Extract structured requirements |
| 2. Verify each requirement with structured reasoning |
| |
| Args: |
| requirements_data: The requirements data (text string or PDF dict) |
| packaging_text: Markdown table extracted from the packaging PDF |
| packaging_data: Structured text with bounding boxes |
| image: The image of the packaging document |
| barcode_data: List of barcode objects with position data |
| metadata: Dictionary containing font, font size, and color metadata |
| model: The Claude model to use |
| |
| Returns: |
| A dictionary containing compliance analysis results |
| """ |
| |
| st.info("Extracting structured requirements...") |
| requirements = self.extract_structured_requirements(requirements_data) |
| |
| if not requirements: |
| st.warning("No requirements found in the document. Please check that your requirements file contains valid requirement statements.") |
| return {"error": "No requirements found", "requirements": [], "verifications": []} |
| |
| st.success(f"Extracted {len(requirements)} requirements") |
| |
| |
| st.info("Verifying requirements...") |
| verifications = [] |
| |
| for i, req in enumerate(requirements): |
| st.text(f"Verifying requirement {i+1}/{len(requirements)}: {req['id']}") |
| |
| |
| verification = self.verify_individual_requirement(req, packaging_text, image, barcode_data, metadata, requirements_data) |
| verifications.append(verification) |
| |
| |
| system_prompt = """You are a regulatory compliance expert. Provide detailed, objective compliance reports.""" |
| |
| |
| compliance_summary = [] |
| for verification in verifications: |
| compliance_summary.append({ |
| 'requirement_id': verification.get('requirement_id', 'Unknown'), |
| 'compliance_status': verification.get('compliance_status', 'UNKNOWN'), |
| 'confidence': verification.get('confidence', 0), |
| 'evidence_count': len(verification.get('evidence_found', [])) |
| }) |
| |
| summary_prompt = f""" |
| Based on the verification of {len(requirements)} requirements, |
| please provide a final compliance summary report. |
| |
| Requirements Summary: |
| {json.dumps([{'id': req['id'], 'description': req['description'], 'category': req['category']} for req in requirements], indent=2)} |
| |
| Compliance Results Summary: |
| {json.dumps(compliance_summary, indent=2)} |
| |
| Format your response in the following template: |
| |
| ## π― **Analysis Requirements** |
| |
| Summarize the overall compliance status with focus on: |
| |
| 1. **Quantitative Metrics**: Count of fully compliant, partially compliant, and non-compliant requirements |
| 2. **Critical Issues**: Most urgent compliance gaps requiring immediate attention |
| 3. **Strategic Recommendations**: Actionable steps for the artwork designer to fix the compliance issues |
| |
| --- |
| |
| ## π **Response Template** |
| |
| ### π **Executive Summary** |
| Provide a single, clear statement of overall compliance status |
| *Example: "Organization achieved 70% compliance (14/20 requirements); moderate risk profile with 3 critical gaps identified."* |
| |
| --- |
| |
| ### π **Compliance Statistics** |
| |
| | **Metric** | **Count** | **Percentage** | |
| |------------|-----------|----------------| |
| | **Total Requirements** | `[total]` | `100%` | |
| | β
**Fully Compliant** | `[count]` | `[%]` | |
| | β οΈ **Partially Compliant** | `[count]` | `[%]` | |
| | β **Non-Compliant** | `[count]` | `[%]` | |
| |
| --- |
| |
| ### π¨ **Priority Findings** |
| |
| List 3-5 highest-severity issues in order of criticality: |
| |
| 1. **[REQ-ID]** - [Brief description of critical issue] |
| 2. **[REQ-ID]** - [Brief description of high-priority gap] |
| 3. **[REQ-ID]** - [Brief description of moderate-priority concern] |
| |
| --- |
| |
| ### π‘ **Targeted Recommendations** |
| |
| For each Priority Finding, provide specific corrective actions: |
| |
| | **Finding** | **Recommended Action** | **Priority** | |
| |-------------|------------------------|--------------| |
| | **[REQ-ID]** | [Specific artwork designer action] | π΄ **Critical** | |
| | **[REQ-ID]** | [Specific artwork designer action] | π‘ **High** | |
| | **[REQ-ID]** | [Specific artwork designer action] | π’ **Medium** | |
| |
| --- |
| |
| ### π **Detailed Assessment Results** |
| |
| *[Provide comprehensive breakdown of each requirement with status and supporting details]* |
| |
| --- |
| |
| ### π **Supporting Evidence** |
| |
| *[Include relevant data, metrics, or documentation that supports the compliance assessment]* |
| |
| |
| """ |
| |
| |
| compliance_report = self.llm.call_claude_api(summary_prompt, system_prompt, model='claude-3-5-haiku-20241022') |
| |
| |
| result = { |
| "requirements": requirements, |
| "verifications": verifications, |
| "compliance_report": compliance_report, |
| "packaging_data": packaging_data, |
| "barcode_data": barcode_data, |
| "metadata": metadata |
| } |
| |
| return result |
|
|