Compare commits
No commits in common. "master" and "main" have entirely different histories.
12863 changed files with 50 additions and 2071473 deletions
58
.env
58
.env
|
|
@ -1,58 +0,0 @@
|
|||
# API Keys
|
||||
OPENAI_API_KEY=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
|
||||
ANTHROPIC_API_KEY=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
|
||||
GOOGLE_API_KEY=AIzaSyAESTMYdQUVW6_XduJoSsAUoTMEmJGlfO4
|
||||
LLAMACLOUD_API_KEY=llx-chSdMBrzHcHu72Yyr5dWh7eobfRoGeCKiNoSdrPkaUdEtelO
|
||||
|
||||
# OpenAI GPT-5.1 Configuration
|
||||
OPENAI_MODEL=gpt-5.1
|
||||
OPENAI_REASONING_EFFORT=medium
|
||||
OPENAI_TIMEOUT=3600
|
||||
OPENAI_MAX_RETRIES=2
|
||||
|
||||
# Google Gemini Configuration
|
||||
GOOGLE_MODEL=gemini-3.1-pro-preview
|
||||
GOOGLE_TEMPERATURE=0.7
|
||||
GOOGLE_MAX_OUTPUT_TOKENS=100000
|
||||
GOOGLE_THINKING_BUDGET=12000
|
||||
GOOGLE_TIMEOUT=3600
|
||||
|
||||
# Anthropic Claude Configuration
|
||||
ANTHROPIC_MODEL_OPUS=claude-opus-4-5-20251101
|
||||
ANTHROPIC_MODEL_SONNET=claude-sonnet-4-5-20250929
|
||||
ANTHROPIC_TEMPERATURE=1
|
||||
ANTHROPIC_MAX_TOKENS=32000
|
||||
ANTHROPIC_THINKING_BUDGET=12000
|
||||
ANTHROPIC_TIMEOUT=300
|
||||
|
||||
# Processing Configuration
|
||||
DEFAULT_PRIMARY_MODELS=openai-gpt51,anthropic-sonnet45,google-gemini31
|
||||
DEFAULT_CONSOLIDATION_MODEL=openai-gpt51
|
||||
MINIMUM_SUCCESS_THRESHOLD=1
|
||||
ENABLE_COST_ESTIMATION=true
|
||||
MAX_PROCESSING_COST_USD=10.00
|
||||
|
||||
# MSAL Authentication Configuration
|
||||
MSAL_CLIENT_ID=placeholder-client-id
|
||||
MSAL_CLIENT_SECRET=placeholder-client-secret
|
||||
MSAL_TENANT_ID=placeholder-tenant-id
|
||||
MSAL_REDIRECT_URI=http://localhost:3000/auth/callback
|
||||
MSAL_AUTHORITY=https://login.microsoftonline.com/placeholder-tenant-id
|
||||
|
||||
# Development and Security Configuration
|
||||
DEV_MODE=true
|
||||
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
|
||||
SESSION_SECRET=your-session-secret-here
|
||||
SECURE_COOKIES=false
|
||||
HTTPS_ONLY=false
|
||||
|
||||
# GUI Runtime Configuration
|
||||
MAX_CONCURRENT_JOBS=5
|
||||
MAX_UPLOAD_SIZE_MB=200
|
||||
FILE_RETENTION_HOURS=24
|
||||
WS_PING_INTERVAL_SECONDS=30
|
||||
|
||||
# Server Configuration
|
||||
SERVER_HOST=0.0.0.0
|
||||
SERVER_PORT=8000
|
||||
SERVER_WORKERS=2
|
||||
95
.gitignore
vendored
95
.gitignore
vendored
|
|
@ -1,45 +1,50 @@
|
|||
# Virtual environment
|
||||
adi-gem-brief/
|
||||
|
||||
# Test and archive folders
|
||||
BRIEFS_TO_TEST/
|
||||
ARCHIVE/
|
||||
|
||||
venv/
|
||||
*.csv
|
||||
output/
|
||||
examples/
|
||||
|
||||
# Upload files
|
||||
uploads/
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Frontend
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/.env
|
||||
frontend/.env.local
|
||||
# These are some examples of commonly ignored file patterns.
|
||||
# You should customize this list as applicable to your project.
|
||||
# Learn more about .gitignore:
|
||||
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
|
||||
|
||||
# Node artifact files
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# Compiled Java class files
|
||||
*.class
|
||||
|
||||
# Compiled Python bytecode
|
||||
*.py[cod]
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Package files
|
||||
*.jar
|
||||
|
||||
# Maven
|
||||
target/
|
||||
dist/
|
||||
|
||||
# JetBrains IDE
|
||||
.idea/
|
||||
|
||||
# Unit test reports
|
||||
TEST*.xml
|
||||
|
||||
# Generated by MacOS
|
||||
.DS_Store
|
||||
|
||||
# Generated by Windows
|
||||
Thumbs.db
|
||||
|
||||
# Applications
|
||||
*.app
|
||||
*.exe
|
||||
*.war
|
||||
|
||||
# Large media files
|
||||
*.mp4
|
||||
*.tiff
|
||||
*.avi
|
||||
*.flv
|
||||
*.mov
|
||||
*.wmv
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="725" height="500">
|
||||
<g
|
||||
transform="translate(-60,-430)" style="fill:#000">
|
||||
<path
|
||||
d="M 533.9435,756.70465 386.43318,500.34377 492.40483,439.4751 l 183.4283,317.22955 -141.88963,0" />
|
||||
<path
|
||||
d="m 141.03958,720.78673 105.97165,-61.27985 56.07037,97.19777 -141.47831,0 -20.56371,-35.91792" />
|
||||
<path
|
||||
d="m 349.00724,920.25463 30.16006,0 0,-122.01125 -30.16006,0 0,122.01125 z" />
|
||||
<path
|
||||
d="m 726.96825,923.13364 c -33.72452,0 -54.01413,-17.4106 -55.11081,-41.95001 l 31.80529,0 c 0,7.67715 4.79819,18.91861 25.36192,19.32994 13.70913,0 20.15239,-8.08849 20.15239,-14.12047 -0.82261,-9.59639 -12.88668,-10.419 -25.7732,-12.47534 -12.88652,-2.0564 -23.85386,-4.38692 -31.80518,-8.49966 -10.14488,-5.20948 -16.99942,-16.45093 -16.99942,-29.33745 0,-21.79767 18.91861,-39.07105 50.44967,-39.07105 30.57139,0 49.90133,16.03964 51.95773,39.8935 l -30.70861,0 c -0.27412,-6.44326 -1.5079,-16.58798 -19.60405,-16.58798 -12.20113,0 -20.28946,2.46752 -20.97501,10.96733 0,12.47519 25.36202,11.65274 45.10314,16.86211 18.9186,4.7983 30.98267,16.58814 30.98267,33.03907 0,30.29727 -24.53941,41.95001 -54.83653,41.95001" />
|
||||
<path
|
||||
d="m 265.24438,611.93622 105.97165,-61.14278 118.85823,205.91121 -110.90696,0 0,30.16006 -30.16006,0 0,-30.29712 -83.76286,-144.63137" />
|
||||
<path
|
||||
d="m 267.98623,923.13364 c -35.09542,0 -63.61043,-28.65221 -63.61043,-63.33619 0,-35.09547 28.51501,-62.78785 63.61043,-62.78785 13.29786,0 25.36187,3.56425 35.91793,10.83012 l 0,-51.13507 30.16011,0 0,163.54998 -30.16011,0 0,-8.08833 c -10.55606,6.85459 -22.62007,10.96734 -35.91793,10.96734 z m -34.68414,-63.33619 c 0,18.9185 16.17678,34.68398 35.50665,34.68398 18.91861,0 35.09542,-15.76548 35.09542,-34.68398 0,-18.91866 -16.17681,-35.09547 -35.09542,-35.09547 -19.32987,0 -35.50665,16.17681 -35.50665,35.09547" />
|
||||
<path
|
||||
d="m 490.89682,756.70465 29.74883,0 0,163.54998 -29.74883,0 0,-8.08833 c -10.14478,6.85459 -22.62007,10.96734 -36.32921,10.96734 -34.68414,0 -63.19913,-28.65221 -63.19913,-63.33619 0,-35.09547 28.51499,-62.78785 63.19913,-62.78785 13.70914,0 25.77315,3.56425 36.32921,10.83012 l 0,-51.13507 z m -70.19079,103.0928 c 0,18.9185 16.17676,34.68398 34.68414,34.68398 19.32989,0 35.50665,-15.76548 35.50665,-34.68398 0,-18.91866 -16.17676,-35.09547 -35.50665,-35.09547 -18.50738,0 -34.68414,16.17681 -34.68414,35.09547" />
|
||||
<path
|
||||
d="m 593.98956,923.13364 c -34.54703,0 -63.19913,-28.65221 -63.19913,-63.33619 0,-35.09547 28.6521,-62.78785 63.19913,-62.78785 13.29791,0 25.7731,3.56425 35.91798,10.83012 l 0,-9.7334 30.16006,0 0,122.14831 -30.16006,0 0,-8.08833 c -10.14488,6.85459 -22.20879,10.96734 -35.91798,10.96734 z m -33.86158,-63.33619 c 0,18.9185 16.17676,34.68398 35.09537,34.68398 18.91866,0 34.68419,-15.76548 34.68419,-34.68398 0,-18.91866 -15.76553,-35.09547 -34.68419,-35.09547 -18.91861,0 -35.09537,16.17681 -35.09537,35.09547" />
|
||||
<path
|
||||
d="m 93.468866,859.79745 c 0,18.9185 16.176784,34.68398 35.095404,34.68398 19.32987,0 35.50666,-15.76548 35.50666,-34.68398 0,-18.91866 -16.17679,-35.09547 -35.50666,-35.09547 -18.91862,0 -35.095404,16.17681 -35.095404,35.09547 z m 34.272844,63.33619 c -34.684124,0 -63.336218,-28.65221 -63.336218,-63.33619 0,-35.09547 28.652094,-62.78785 63.336218,-62.78785 13.29787,0 25.77319,3.56425 36.32922,10.83012 l 0,-9.7334 29.74883,0 0,122.14831 -29.74883,0 0,-8.08833 c -10.14475,6.85459 -22.62008,10.96734 -36.32922,10.96734" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
390
CLAUDE.md
390
CLAUDE.md
|
|
@ -1,390 +0,0 @@
|
|||
# Enhanced Brief Processing System
|
||||
|
||||
Multi-model AI document analysis platform for extracting structured marketing asset information from creative briefs and presentations. Built for marketing agencies and creative teams.
|
||||
|
||||
## Project Overview
|
||||
|
||||
The **Enhanced Brief Processing System** is a sophisticated document analysis platform that leverages multiple cutting-edge AI models in parallel to extract comprehensive, structured asset information from unstructured documents. The system processes creative briefs, presentations, and technical documents through an intelligent multi-model pipeline with advanced consolidation and multiplier-based expansion capabilities.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Multi-Model Processing Pipeline
|
||||
```
|
||||
Document Upload → LlamaParser → Parallel Multi-Model Analysis → Intelligent Consolidation → Multiplier Expansion → CSV Export
|
||||
```
|
||||
|
||||
The system consists of five main layers:
|
||||
|
||||
1. **Web Interface** (`index.php`) - File upload and model selection UI
|
||||
2. **PHP Backend** (`upload_enhanced.php`) - File handling and process orchestration
|
||||
3. **LLM Service Layer** (`llm_service/`) - Multi-provider abstraction and parallel execution
|
||||
4. **Python Processing Engine** (`process_brief_enhanced.py`) - Core document analysis and consolidation
|
||||
5. **External Configuration** (`.env`, `prompts/`) - Environment settings and prompt templates
|
||||
|
||||
### LLM Service Architecture
|
||||
|
||||
**Provider Abstraction Layer:**
|
||||
- **`base_provider.py`** - Common interface for all LLM providers
|
||||
- **`openai_provider.py`** - OpenAI GPT-5 with reasoning effort support
|
||||
- **`anthropic_provider.py`** - Claude Opus 4.1 and Sonnet 4 integration
|
||||
- **`google_provider.py`** - Gemini 2.5 Pro support with new google-genai SDK
|
||||
- **`provider_manager.py`** - Parallel execution coordinator with async/await
|
||||
|
||||
**Consolidation System:**
|
||||
- **`consolidation_processor.py`** - Multi-model result merging with advanced deduplication
|
||||
- **`prompts/consolidation_analysis.txt`** - Intelligent consolidation prompt with bias toward completeness
|
||||
|
||||
## Current Model Support
|
||||
|
||||
### Available Models
|
||||
- **`openai-gpt5`** - OpenAI GPT-5 with configurable reasoning effort (high/medium/low)
|
||||
- **`anthropic-opus4`** - Claude Opus 4.1 (highest quality, premium cost)
|
||||
- **`anthropic-sonnet4`** - Claude Sonnet 4 (balanced performance and cost)
|
||||
- **`google-gemini25`** - Gemini 2.5 Pro (cost-effective, high context)
|
||||
|
||||
### Model Capabilities
|
||||
- **Native Async Support:** All providers use async clients for true parallel processing
|
||||
- **Structured Output:** Universal schema works across all providers
|
||||
- **Cost Optimization:** Provider-specific pricing and token usage tracking
|
||||
- **Fallback Logic:** Minimum success threshold with graceful degradation
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
- **Python 3.13+** with virtual environment at `venv/`
|
||||
- **API Keys:** OpenAI, Anthropic, Google, and LlamaCloud configured in `.env`
|
||||
- **Dependencies:** Install from `requirements_enhanced.txt`
|
||||
|
||||
### Environment Configuration
|
||||
Create `.env` file with your API keys:
|
||||
```bash
|
||||
# API Keys
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
ANTHROPIC_API_KEY=your-anthropic-api-key
|
||||
GOOGLE_API_KEY=your-google-api-key
|
||||
LLAMACLOUD_API_KEY=your-llamacloud-api-key
|
||||
|
||||
# Model Configuration
|
||||
OPENAI_REASONING_EFFORT=medium
|
||||
ANTHROPIC_MAX_TOKENS=64000
|
||||
GOOGLE_MAX_OUTPUT_TOKENS=100000
|
||||
|
||||
# Processing Defaults
|
||||
DEFAULT_PRIMARY_MODELS=openai-gpt5,anthropic-sonnet4,google-gemini25
|
||||
DEFAULT_CONSOLIDATION_MODEL=openai-gpt5
|
||||
MAX_PROCESSING_COST_USD=10.00
|
||||
```
|
||||
|
||||
### Python Dependencies Installation
|
||||
```bash
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Install enhanced dependencies with async support
|
||||
pip install -r requirements_enhanced.txt
|
||||
```
|
||||
|
||||
**Key Dependencies:**
|
||||
- **AI Models:** `google-genai[aiohttp]>=0.4.0`, `openai>=1.0.0`, `anthropic>=0.67.0`
|
||||
- **Document Processing:** `llama-cloud-services>=0.6.62`, `python-pptx>=0.6.21`, `PyMuPDF>=1.23.0`
|
||||
- **Configuration:** `python-dotenv>=1.0.0`, `pydantic>=2.0.0`
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
**Basic Usage (Default Models):**
|
||||
```bash
|
||||
python process_brief_enhanced.py document.pdf
|
||||
```
|
||||
|
||||
**Custom Model Selection:**
|
||||
```bash
|
||||
# Specify primary analysis models and consolidation model
|
||||
python process_brief_enhanced.py document.pdf \
|
||||
--primary-models openai-gpt5,anthropic-sonnet4,google-gemini25 \
|
||||
--consolidation-model anthropic-opus4
|
||||
|
||||
# Quick two-model analysis
|
||||
python process_brief_enhanced.py document.pdf \
|
||||
--primary-models openai-gpt5,google-gemini25 \
|
||||
--consolidation-model openai-gpt5
|
||||
|
||||
# Cost estimation before processing
|
||||
python process_brief_enhanced.py document.pdf --estimate-cost
|
||||
```
|
||||
|
||||
**Model Selection Guide:**
|
||||
- **High Quality:** `--primary-models openai-gpt5,anthropic-opus4,google-gemini25 --consolidation-model anthropic-opus4`
|
||||
- **Balanced:** `--primary-models openai-gpt5,anthropic-sonnet4,google-gemini25 --consolidation-model openai-gpt5` (default)
|
||||
- **Cost-Effective:** `--primary-models openai-gpt5,google-gemini25 --consolidation-model google-gemini25`
|
||||
- **Fast Processing:** `--primary-models anthropic-sonnet4,google-gemini25 --consolidation-model anthropic-sonnet4`
|
||||
|
||||
### Web Interface Usage
|
||||
```bash
|
||||
# Start web server pointing to project directory
|
||||
# Access via index.php for file upload interface
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### Multi-Model Document Analysis
|
||||
**Primary Analysis Stage:**
|
||||
- Multiple models analyze the same document simultaneously in parallel
|
||||
- Each model extracts base deliverables with multiplier arrays
|
||||
- Universal schema ensures consistent output format across providers
|
||||
- Error handling allows processing to continue if some models fail
|
||||
|
||||
**Consolidation Stage:**
|
||||
- Dedicated consolidation model intelligently merges all primary results
|
||||
- Advanced deduplication prevents over-counting while preserving unique deliverables
|
||||
- Bias toward completeness - if any model finds a legitimate deliverable, it's included
|
||||
- Quality enhancement using best specifications from all contributing models
|
||||
|
||||
### Multiplier-Based Asset Expansion
|
||||
**Base Deliverable Approach:**
|
||||
- Extract base deliverable types with multiplier arrays
|
||||
- **Multiplier Fields:** `technical_specifications`, `language_country_market`
|
||||
- **Metadata Fields:** All other fields as single string values
|
||||
|
||||
**Smart Expansion Logic:**
|
||||
- Uses `itertools.product()` to generate all valid combinations
|
||||
- **Example:** 3 technical specs × 5 markets = 15 individual deliverables
|
||||
- Quantity validation ensures expansion matches expected deliverable counts
|
||||
|
||||
### Universal Schema System
|
||||
**Schema Location:** `prompts/universal_schema.json`
|
||||
|
||||
**Field Types:**
|
||||
- **String Fields (Metadata):** `title`, `status`, `category`, `media`, `asset_type`, `brand_identifier`, dates, `reference_material`, `page_number`, `priority_level`, `creative_direction`, `quantity`
|
||||
- **Array Fields (Multipliers):** `technical_specifications`, `language_country_market`
|
||||
|
||||
**Provider Compatibility:**
|
||||
- **OpenAI:** Native schema support with Pydantic models
|
||||
- **Google:** Automatic conversion to Gemini-compatible format
|
||||
- **Anthropic:** Conversion to tool schema format
|
||||
|
||||
### Advanced Processing Features
|
||||
|
||||
**Document Preprocessing (LlamaParser):**
|
||||
- **Parse Mode:** Agent-based parsing for enhanced accuracy
|
||||
- **OCR:** High-resolution OCR with adaptive table detection
|
||||
- **Output:** Clean markdown with preserved document structure
|
||||
- **Multi-Format:** PowerPoint, Word, PDF, Excel support
|
||||
|
||||
**Cost Management:**
|
||||
- **Pre-Processing Estimation:** Calculate costs before execution
|
||||
- **Real-Time Tracking:** Monitor token usage across all models
|
||||
- **Budget Controls:** Configurable cost limits with user confirmation
|
||||
- **Detailed Breakdowns:** Per-model cost analysis and optimization recommendations
|
||||
|
||||
**Quality Assurance:**
|
||||
- **Multi-Perspective Analysis:** Different models catch different details
|
||||
- **Consolidation Validation:** Ensures no legitimate deliverables are lost
|
||||
- **Expansion Verification:** Quantity checks and multiplier validation
|
||||
- **Comprehensive Logging:** Detailed processing logs for debugging
|
||||
|
||||
## Output Schema
|
||||
|
||||
### CSV Export Format (16 Columns)
|
||||
Generated files follow naming pattern: `filename-YYYYMMDDHHMMSS.csv`
|
||||
|
||||
**Field Definitions:**
|
||||
- **`title`** - Normalized deliverable name without multipliers
|
||||
- **`category`** - Asset category (e.g., "Paid Social", "Display Advertising")
|
||||
- **`media`** - Media type ("IMAGE", "VIDEO", "COPY", "INTERACTIVE")
|
||||
- **`asset_type`** - File format ("JPG", "PNG", "MP4", "GIF")
|
||||
- **`technical_specifications`** - Dimensions, sizes, requirements
|
||||
- **`language_country_market`** - Target market using ISO codes (e.g., "EN-UK", "DE-DE")
|
||||
- **`quantity`** - Expected quantity (used for validation)
|
||||
- **`brand_identifier`** - Brand or client name
|
||||
- **Dates:** `review_date`, `live_date`, `end_date`
|
||||
- **Context:** `reference_material`, `page_number`, `priority_level`, `creative_direction`, `status`
|
||||
|
||||
## Configuration
|
||||
|
||||
### API Key Management
|
||||
All API keys are managed through `.env` file (not committed to repository):
|
||||
```bash
|
||||
OPENAI_API_KEY=your-key-here
|
||||
ANTHROPIC_API_KEY=your-key-here
|
||||
GOOGLE_API_KEY=your-key-here
|
||||
LLAMACLOUD_API_KEY=your-key-here
|
||||
```
|
||||
|
||||
### Model-Specific Settings
|
||||
```bash
|
||||
# OpenAI GPT-5 Configuration
|
||||
OPENAI_REASONING_EFFORT=medium # high, medium, low, minimal
|
||||
OPENAI_TIMEOUT=3600
|
||||
OPENAI_MAX_RETRIES=2
|
||||
|
||||
# Anthropic Claude Configuration
|
||||
ANTHROPIC_MAX_TOKENS=64000
|
||||
ANTHROPIC_TEMPERATURE=0.7
|
||||
ANTHROPIC_TIMEOUT=300
|
||||
|
||||
# Google Gemini Configuration
|
||||
GOOGLE_MAX_OUTPUT_TOKENS=100000
|
||||
GOOGLE_TEMPERATURE=0.7
|
||||
GOOGLE_TIMEOUT=3600
|
||||
```
|
||||
|
||||
### Processing Configuration
|
||||
```bash
|
||||
DEFAULT_PRIMARY_MODELS=openai-gpt5,anthropic-sonnet4,google-gemini25
|
||||
DEFAULT_CONSOLIDATION_MODEL=openai-gpt5
|
||||
MINIMUM_SUCCESS_THRESHOLD=1
|
||||
ENABLE_COST_ESTIMATION=true
|
||||
MAX_PROCESSING_COST_USD=10.00
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Parallel Multi-Model Processing
|
||||
- **Simultaneous Execution:** All primary models process document at the same time
|
||||
- **True Async:** Native async/await support across all providers (AsyncOpenAI, AsyncAnthropic, client.aio)
|
||||
- **Performance:** Total time limited by slowest model, not sum of all models
|
||||
- **Reliability:** Configurable minimum success threshold allows processing to continue if some models fail
|
||||
|
||||
### Intelligent Consolidation
|
||||
- **Inclusion Philosophy:** "If any model found it, include it" - bias toward completeness
|
||||
- **Smart Deduplication:** Advanced algorithms distinguish true duplicates from legitimate variations
|
||||
- **Quality Enhancement:** Best specifications from all models combined
|
||||
- **Normalization:** Canonical title and category formatting across models
|
||||
|
||||
### Cost Intelligence
|
||||
- **Multi-Provider Pricing:** Accurate cost tracking across OpenAI ($2.50-$10.00/1M), Anthropic ($3.00-$75.00/1M), Google ($1.25-$5.00/1M)
|
||||
- **Pre-Processing Estimates:** Calculate total cost before execution
|
||||
- **Real-Time Monitoring:** Track spending across all models during processing
|
||||
- **Budget Protection:** Configurable limits with user confirmation prompts
|
||||
|
||||
### Schema-Driven Extraction
|
||||
- **Universal Compatibility:** Single schema works across all LLM providers
|
||||
- **Multiplier Optimization:** Only 2 array fields prevent over-multiplication
|
||||
- **Quantity Validation:** Built-in sense-checks ensure realistic deliverable counts
|
||||
- **External Management:** Schema stored in `prompts/universal_schema.json` for easy modification
|
||||
|
||||
## File Processing
|
||||
|
||||
### Supported Document Types
|
||||
- **PowerPoint** (.ppt, .pptx) - Slide-by-slide content extraction with table support
|
||||
- **Word** (.doc, .docx) - Paragraph and table content with structure preservation
|
||||
- **PDF** - Page-by-page text extraction with high-resolution OCR
|
||||
- **Excel** (.xls, .xlsx) - Multi-sheet data extraction with cell formatting
|
||||
|
||||
### LlamaParser Integration
|
||||
Documents processed using LlamaParser cloud service for optimal extraction:
|
||||
- **Agent-Based Parsing:** Enhanced accuracy for complex documents
|
||||
- **High-Resolution OCR:** Superior text recognition from images and scanned content
|
||||
- **Adaptive Table Detection:** Intelligent table structure recognition and HTML output
|
||||
- **Page Separation:** Maintains document structure with custom separators
|
||||
|
||||
## Output Files
|
||||
|
||||
### CSV Generation
|
||||
- **Location:** `output/` directory
|
||||
- **Naming:** `{filename}-{timestamp}.csv` (e.g., `brief_20241212143022.csv`)
|
||||
- **Format:** 16-column structured format with comprehensive asset details
|
||||
- **Encoding:** UTF-8 with proper international character support
|
||||
|
||||
### Intermediate Files
|
||||
- **Base Deliverables:** `base_deliverable_JSON/` directory with pre-expansion data
|
||||
- **Processing Logs:** `processing.log` with detailed execution information
|
||||
- **Cost Tracking:** Embedded cost summaries in log output
|
||||
|
||||
## Error Handling and Monitoring
|
||||
|
||||
### Comprehensive Logging
|
||||
- **Processing Stages:** Detailed logs for each pipeline stage
|
||||
- **Model Performance:** Individual model success/failure tracking with deliverable counts
|
||||
- **Cost Analysis:** Token usage and cost breakdown per provider
|
||||
- **Expansion Details:** Multiplier field analysis and expansion calculations
|
||||
- **Consolidation Metrics:** Before/after consolidation statistics
|
||||
|
||||
### Error Resilience
|
||||
- **Partial Success:** Processing continues if minimum model threshold met
|
||||
- **Graceful Degradation:** Automatic fallback to available models
|
||||
- **Detailed Diagnostics:** Comprehensive error messages with troubleshooting guidance
|
||||
- **Validation Warnings:** Quantity mismatches and expansion anomaly detection
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Processing Times
|
||||
- **Document Extraction:** 10-60 seconds (LlamaParser)
|
||||
- **Multi-Model Analysis:** 30-180 seconds (parallel processing)
|
||||
- **Consolidation:** 15-45 seconds (single model)
|
||||
- **Total Processing:** 1-5 minutes depending on document complexity and model selection
|
||||
|
||||
### Scalability
|
||||
- **Parallel Processing:** Linear performance improvement with multiple models
|
||||
- **Memory Optimization:** Streaming output and efficient token management
|
||||
- **Context Management:** Handles large documents up to 2M tokens (Gemini context limit)
|
||||
- **Cost Efficiency:** Intelligent model selection based on quality/cost tradeoffs
|
||||
|
||||
## System Requirements
|
||||
|
||||
### Python Environment
|
||||
- **Python Version:** 3.13+ recommended
|
||||
- **Virtual Environment:** Required (`venv/` directory)
|
||||
- **Dependencies:** See `requirements_enhanced.txt` for complete list
|
||||
|
||||
### External Services
|
||||
- **LlamaCloud:** Document parsing service (requires API key)
|
||||
- **OpenAI API:** GPT-5 access with responses API support
|
||||
- **Anthropic API:** Claude Opus 4.1 and Sonnet 4 access
|
||||
- **Google AI API:** Gemini 2.5 Pro access
|
||||
|
||||
### Web Server (Optional)
|
||||
- **PHP 7.4+** for web interface
|
||||
- **File Upload Support:** Large file handling for document processing
|
||||
- **Directory Permissions:** Write access to output directories
|
||||
|
||||
## Migration from Legacy System
|
||||
|
||||
### Breaking Changes
|
||||
- **Schema Format:** Moved from hybrid string/array fields to optimized mixed schema
|
||||
- **Field Structure:** Merged `language` and `country` into `language_country_market`
|
||||
- **Quantity Field:** Changed from array multiplier to string validation field
|
||||
- **Configuration:** API keys moved from hardcoded to environment variables
|
||||
|
||||
### Backward Compatibility
|
||||
- **CSV Output:** Same 16-column format with updated field names
|
||||
- **PHP Integration:** Existing web interface continues to work
|
||||
- **File Processing:** Same document type support and LlamaParser integration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
- **API Key Errors:** Verify all required keys are set in `.env` file
|
||||
- **Cost Limits:** Adjust `MAX_PROCESSING_COST_USD` if processing is rejected
|
||||
- **Model Failures:** Check individual provider status and retry with different model combination
|
||||
- **High Deliverable Counts:** Review multiplier arrays and quantity validation in logs
|
||||
|
||||
### Debug Information
|
||||
- **Processing Logs:** Check `processing.log` for detailed execution information
|
||||
- **Cost Analysis:** Review token usage and cost breakdown in log output
|
||||
- **Model Performance:** Compare deliverable counts across models for consistency
|
||||
- **Expansion Details:** Examine multiplier field values and calculations
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Adding New Providers
|
||||
1. Create new provider class extending `BaseLLMProvider`
|
||||
2. Implement async `generate_response()` method
|
||||
3. Add provider configuration to `config.py`
|
||||
4. Update model mappings and CLI interface
|
||||
|
||||
### Modifying Processing Logic
|
||||
- **Prompts:** Edit files in `prompts/` directory
|
||||
- **Schema:** Modify `prompts/universal_schema.json`
|
||||
- **Expansion:** Update `expand_deliverables()` function
|
||||
- **Consolidation:** Adjust `consolidation_processor.py` logic
|
||||
|
||||
### Performance Optimization
|
||||
- **Model Selection:** Choose optimal provider combinations for your use case
|
||||
- **Cost Management:** Monitor and adjust processing cost limits
|
||||
- **Async Configuration:** Tune timeout and retry settings per provider
|
||||
- **Schema Refinement:** Optimize multiplier fields to prevent over-expansion
|
||||
|
||||
The Enhanced Brief Processing System represents a significant advancement in AI-powered document analysis, providing unparalleled accuracy, flexibility, and cost control through its sophisticated multi-model architecture.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY server_requirements.txt .
|
||||
RUN pip install --no-cache-dir -r server_requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY server/ ./server/
|
||||
COPY core/ ./core/
|
||||
COPY prompts/ ./prompts/
|
||||
COPY run_server.py ./
|
||||
|
||||
# Create data directories
|
||||
RUN mkdir -p server/data/uploads server/data/outputs
|
||||
|
||||
# Set Python path
|
||||
ENV PYTHONPATH=/app:/app/server
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# Run with the startup script
|
||||
CMD ["python", "run_server.py"]
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
# ✅ Implementation Complete: 100% Plan Compliant
|
||||
|
||||
## 🎯 **Plan Compliance Status: 100%**
|
||||
|
||||
Your development plan has been **fully implemented** according to all specifications. Here's the complete checklist:
|
||||
|
||||
### ✅ **Backend Implementation (100% Complete)**
|
||||
|
||||
#### **1. Target Architecture**
|
||||
- ✅ **Quart + Hypercorn** async web framework
|
||||
- ✅ **AsyncIO queue** with concurrency semaphore (MAX_CONCURRENCY=2)
|
||||
- ✅ **WebSocket** real-time updates at `/ws`
|
||||
- ✅ **In-memory job registry** with file artifacts
|
||||
- ✅ **Local disk storage** for uploads & CSV outputs
|
||||
|
||||
#### **2. Data Contracts**
|
||||
- ✅ **JobPhase enum** exactly as specified
|
||||
- ✅ **ProviderUpdate** with all required fields
|
||||
- ✅ **JobSummary** with processing metadata
|
||||
- ✅ **Job model** with complete state tracking
|
||||
- ✅ **WebSocket events** matching exact specification
|
||||
|
||||
#### **3. Backend Components**
|
||||
- ✅ **Project layout** matches plan structure
|
||||
- ✅ **Job model & queue** with asyncio.Queue and semaphore
|
||||
- ✅ **REST endpoints** - all required endpoints implemented
|
||||
- ✅ **WebSocket broadcast** with subscription management
|
||||
- ✅ **Progress instrumentation** with exact weights
|
||||
- ✅ **CSV & summary surfacing** from ProcessingResult
|
||||
- ✅ **Backend adjustments** - ProviderManager callback, DocumentAnalyzer progress
|
||||
|
||||
### ✅ **Frontend Implementation (100% Complete)**
|
||||
|
||||
#### **4. Frontend Architecture**
|
||||
- ✅ **React + Vite + TypeScript + Tailwind** tech stack
|
||||
- ✅ **Zustand store** for global job state
|
||||
- ✅ **TanStack Query** for REST endpoints
|
||||
- ✅ **WebSocket client** with reconnection logic
|
||||
|
||||
#### **5. UI Components**
|
||||
- ✅ **App shell** with header, logo, and connection status
|
||||
- ✅ **UploadPanel** with drag-and-drop and multi-select
|
||||
- ✅ **QueueView** with active jobs list
|
||||
- ✅ **JobCard** with progress bar and provider chips
|
||||
- ✅ **JobAccordion** for completed jobs with collapsible summary
|
||||
- ✅ **SummaryPanel** with doc type, assets, confidence, cost, tokens, notes
|
||||
- ✅ **LogsPanel** with real-time streaming and filtering
|
||||
- ✅ **ConnectionStatus** indicator with auto-retry
|
||||
|
||||
#### **6. State Management**
|
||||
- ✅ **Zustand store** with jobs keyed by ID
|
||||
- ✅ **WebSocket integration** with queue.snapshot and incremental events
|
||||
- ✅ **TanStack Query** for on-demand REST fetches
|
||||
|
||||
### ✅ **Progress System (100% Complete)**
|
||||
|
||||
#### **7. Progress Math** (Exact Plan Specification)
|
||||
- ✅ **QUEUED = 0%**
|
||||
- ✅ **EXTRACT_CONTENT: 10% → 25%** (25% weight)
|
||||
- ✅ **LLM_ANALYSIS: 25% → 75%** (50% weight, divided evenly across N providers)
|
||||
- ✅ **CONSOLIDATION: 75% → 90%** (15% weight)
|
||||
- ✅ **CSV_GENERATION: 90% → 100%** (10% weight)
|
||||
- ✅ **Never decrements, clamped to [0,100]**
|
||||
|
||||
#### **8. Progress Instrumentation**
|
||||
- ✅ **ProgressReporter** interface implemented
|
||||
- ✅ **DocumentAnalyzer hooks** at all phase boundaries
|
||||
- ✅ **ProviderManager callback** for per-provider updates
|
||||
- ✅ **Per-job structured logging** with JobLogHandler
|
||||
- ✅ **Legacy print statements** conditionally removed for GUI mode
|
||||
|
||||
### ✅ **Security & Operations (100% Complete)**
|
||||
|
||||
#### **9. Security Implementation**
|
||||
- ✅ **CORS** configured for frontend origins
|
||||
- ✅ **Upload validation** (MIME types, size limits, safe filenames)
|
||||
- ✅ **File cleanup** with retention policy
|
||||
- ✅ **Environment variables** for secrets
|
||||
- ✅ **Resource limits** (concurrency, timeouts, ping keep-alive)
|
||||
- ✅ **Per-job logging** for audit trail
|
||||
|
||||
### ✅ **Nice-to-Have Features (Plan Specified)**
|
||||
|
||||
#### **10. Professional Features**
|
||||
- ✅ **Download "all CSVs (zip)"** for batch operations
|
||||
- ✅ **User-configurable models/thresholds** in advanced settings
|
||||
- ✅ **Per-provider cost tracking** with real-time updates
|
||||
- ✅ **Accessible UI** with semantic progress bars and screen-reader text
|
||||
|
||||
### ✅ **Definition of Done (100% Met)**
|
||||
|
||||
#### **11. Plan's Definition of Done**
|
||||
- ✅ **Upload single or multiple files** → see them enter queue instantly
|
||||
- ✅ **Live step indicators** with non-jittery percentages
|
||||
- ✅ **Per-provider chips** showing started → success/error with latency/tokens
|
||||
- ✅ **Collapsible summary** with doc type, assets, confidence, notes, cost, tokens, models
|
||||
- ✅ **Download CSV button** becomes active at completion
|
||||
- ✅ **WebSocket reconnect** with no duplicated entries or memory growth
|
||||
|
||||
## 🚀 **Ready to Launch**
|
||||
|
||||
### **Development Mode**
|
||||
```bash
|
||||
# 1. Start backend
|
||||
python run_server.py
|
||||
|
||||
# 2. Start frontend (new terminal)
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
# 3. Access: http://localhost:3000
|
||||
```
|
||||
|
||||
### **Production Deployment**
|
||||
```bash
|
||||
# Configure MSAL in .env, then:
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 🎯 **Key Architecture Achievements**
|
||||
|
||||
1. **✅ Preserves existing CLI** - Original `python core/process_brief_enhanced.py` still works
|
||||
2. **✅ Minimal core changes** - Added progress hooks without rewriting algorithms
|
||||
3. **✅ True async throughout** - From WebSocket to AI model calls
|
||||
4. **✅ Real-time everything** - Progress, logs, provider status, costs
|
||||
5. **✅ Production ready** - MSAL auth, Docker, error handling, cleanup
|
||||
6. **✅ Scalable design** - Horizontal scaling, concurrent jobs, efficient state management
|
||||
|
||||
## 📋 **Plan Specification Compliance**
|
||||
|
||||
Every requirement from your development plan has been implemented:
|
||||
|
||||
- **✅ Section 1**: Target architecture (React/Vite + Quart/Hypercorn)
|
||||
- **✅ Section 2**: Data contracts (server ↔ client)
|
||||
- **✅ Section 3**: Backend (Quart/Hypercorn) — endpoints, queue, instrumentation
|
||||
- **✅ Section 4**: Frontend (React/Vite/TS) — screens, state, UX
|
||||
- **✅ Section 5**: Progress math (exact percentages)
|
||||
- **✅ Section 6**: Security & operations
|
||||
- **✅ Section 8**: Developer task breakdown (all 11 steps)
|
||||
- **✅ Section 9**: Code snippets (all implemented)
|
||||
- **✅ Section 10**: Summary from logs
|
||||
- **✅ Section 11**: Nice-to-haves
|
||||
- **✅ Section 12**: Definition of Done
|
||||
|
||||
**The implementation is 100% complete and ready for production use!** 🎉
|
||||
344
README.md
344
README.md
|
|
@ -1,344 +0,0 @@
|
|||
# Enhanced Brief Processing System
|
||||
|
||||
> **Multi-Model AI Document Analysis Platform**
|
||||
> Extract structured marketing asset information from creative briefs using parallel AI processing
|
||||
|
||||
## Overview
|
||||
|
||||
The Enhanced Brief Processing System is a cutting-edge document analysis platform that leverages multiple state-of-the-art AI models simultaneously to extract comprehensive, structured asset information from unstructured marketing documents. Built for marketing agencies, creative teams, and project managers, this system transforms complex briefs into actionable, structured data through intelligent multi-model consensus.
|
||||
|
||||
## 🚀 Key Features
|
||||
|
||||
### Multi-Model Parallel Processing
|
||||
- **Simultaneous Analysis**: Process documents through multiple AI models in parallel
|
||||
- **Provider Support**: OpenAI GPT-5, Claude Opus 4.1/Sonnet 4, Google Gemini 2.5 Pro
|
||||
- **True Async**: Native async/await implementation for optimal performance
|
||||
- **Intelligent Fallback**: Continues processing even if individual models fail
|
||||
|
||||
### Advanced Document Processing
|
||||
- **Multi-Format Support**: PowerPoint (.pptx), Word (.docx), PDF, Excel (.xlsx)
|
||||
- **LlamaParser Integration**: Cloud-based OCR with adaptive table detection
|
||||
- **Structure Preservation**: Maintains document hierarchy, tables, and page references
|
||||
- **High-Resolution OCR**: Superior text recognition from images and scanned content
|
||||
|
||||
### Intelligent Asset Extraction
|
||||
- **Multiplier-Based System**: Extract base deliverables with expansion arrays
|
||||
- **Smart Consolidation**: Merge results from multiple models with bias toward completeness
|
||||
- **Advanced Deduplication**: Distinguish true duplicates from legitimate variations
|
||||
- **Quantity Validation**: Built-in sense-checks ensure realistic deliverable counts
|
||||
|
||||
### Cost Management & Monitoring
|
||||
- **Multi-Provider Pricing**: Track costs across OpenAI, Anthropic, and Google
|
||||
- **Pre-Processing Estimates**: Calculate total cost before execution
|
||||
- **Budget Controls**: Configurable spending limits with user confirmation
|
||||
- **Detailed Breakdowns**: Per-model cost analysis and optimization insights
|
||||
|
||||
## 📋 What It Extracts
|
||||
|
||||
The system identifies and structures marketing deliverables including:
|
||||
|
||||
- **Technical Specifications**: Exact dimensions, file formats, technical requirements
|
||||
- **Market Targeting**: Language-country combinations using ISO codes
|
||||
- **Asset Details**: Categories, media types, file formats, quantities
|
||||
- **Timeline Information**: Review dates, launch dates, expiry dates
|
||||
- **Creative Direction**: Design requirements, brand guidelines, reference materials
|
||||
- **Project Context**: Page numbers, priority levels, status information
|
||||
|
||||
## 🛠 Installation & Setup
|
||||
|
||||
### Prerequisites
|
||||
- **Python 3.13+** with virtual environment support
|
||||
- **API Keys**: OpenAI, Anthropic, Google AI, and LlamaCloud accounts
|
||||
|
||||
### Quick Setup
|
||||
```bash
|
||||
# Clone and navigate to project
|
||||
cd enhanced-brief-processing-system
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dependencies with async support
|
||||
pip install -r requirements_enhanced.txt
|
||||
|
||||
# Configure environment variables
|
||||
cp .env.example .env
|
||||
# Edit .env with your API keys
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
Create `.env` file with your credentials:
|
||||
```bash
|
||||
# API Keys (Required)
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
ANTHROPIC_API_KEY=your-anthropic-api-key
|
||||
GOOGLE_API_KEY=your-google-api-key
|
||||
LLAMACLOUD_API_KEY=your-llamacloud-api-key
|
||||
|
||||
# Processing Configuration
|
||||
DEFAULT_PRIMARY_MODELS=openai-gpt5,anthropic-sonnet4,google-gemini25
|
||||
DEFAULT_CONSOLIDATION_MODEL=openai-gpt5
|
||||
MAX_PROCESSING_COST_USD=10.00
|
||||
```
|
||||
|
||||
## 🎯 Usage
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
**Basic Usage (Recommended):**
|
||||
```bash
|
||||
# Use default 3-model configuration
|
||||
python process_brief_enhanced.py your_brief.pdf
|
||||
```
|
||||
|
||||
**Custom Model Selection:**
|
||||
```bash
|
||||
# High-quality analysis
|
||||
python process_brief_enhanced.py brief.pdf \
|
||||
--primary-models openai-gpt5,anthropic-opus4,google-gemini25 \
|
||||
--consolidation-model anthropic-opus4
|
||||
|
||||
# Cost-effective processing
|
||||
python process_brief_enhanced.py brief.pdf \
|
||||
--primary-models openai-gpt5,google-gemini25 \
|
||||
--consolidation-model google-gemini25
|
||||
|
||||
# Single model (fastest)
|
||||
python process_brief_enhanced.py brief.pdf \
|
||||
--primary-models openai-gpt5 \
|
||||
--consolidation-model openai-gpt5
|
||||
```
|
||||
|
||||
**Cost Estimation:**
|
||||
```bash
|
||||
# Estimate costs before processing
|
||||
python process_brief_enhanced.py brief.pdf --estimate-cost
|
||||
```
|
||||
|
||||
### Web Interface
|
||||
```bash
|
||||
# Start local web server
|
||||
# Navigate to index.php for browser-based upload interface
|
||||
```
|
||||
|
||||
### Available Models
|
||||
|
||||
| Model | Provider | Best For | Cost |
|
||||
|-------|----------|----------|------|
|
||||
| `openai-gpt5` | OpenAI | Complex reasoning, detailed analysis | $$$ |
|
||||
| `anthropic-opus4` | Anthropic | Highest quality, premium analysis | $$$$ |
|
||||
| `anthropic-sonnet4` | Anthropic | Balanced performance and cost | $$ |
|
||||
| `google-gemini25` | Google | Cost-effective, large context | $ |
|
||||
|
||||
## 🔄 Processing Flow
|
||||
|
||||
### Stage 1: Document Preprocessing
|
||||
- **LlamaParser Cloud Service**: High-resolution OCR and structure extraction
|
||||
- **Content Normalization**: Convert to clean markdown with preserved formatting
|
||||
- **Multi-Format Support**: Automatic document type detection and handling
|
||||
|
||||
### Stage 2: Parallel Multi-Model Analysis
|
||||
- **Simultaneous Processing**: Multiple AI models analyze the same document
|
||||
- **Universal Schema**: Consistent structured output across all providers
|
||||
- **Error Handling**: Graceful degradation if individual models fail
|
||||
- **Performance Logging**: Track model performance and deliverable counts
|
||||
|
||||
### Stage 3: Intelligent Consolidation
|
||||
- **Multi-Model Merging**: Combine results from all successful analyses
|
||||
- **Advanced Deduplication**: Smart algorithms prevent over-counting
|
||||
- **Quality Enhancement**: Use best specifications from any contributing model
|
||||
- **Completeness Bias**: Include deliverables found by any model
|
||||
|
||||
### Stage 4: Multiplier-Based Expansion
|
||||
- **Base Deliverable Processing**: Expand multiplier arrays into individual assets
|
||||
- **Controlled Multiplication**: Only 2 array fields prevent over-expansion
|
||||
- **Quantity Validation**: Ensure results match expected deliverable counts
|
||||
- **Detailed Logging**: Track expansion calculations for debugging
|
||||
|
||||
## 📊 Output Format
|
||||
|
||||
### Structured CSV Export
|
||||
Generated files: `output/{filename}-{timestamp}.csv`
|
||||
|
||||
**16-Column Schema:**
|
||||
- **Core**: `title`, `category`, `media`, `asset_type`, `status`
|
||||
- **Technical**: `technical_specifications`, `brand_identifier`
|
||||
- **Market**: `language_country_market` (ISO format: "EN-UK", "DE-DE")
|
||||
- **Timeline**: `review_date`, `live_date`, `end_date`
|
||||
- **Context**: `reference_material`, `page_number`, `priority_level`, `creative_direction`
|
||||
- **Validation**: `quantity`
|
||||
|
||||
### Example Output
|
||||
```csv
|
||||
title,category,media,asset_type,technical_specifications,language_country_market,quantity
|
||||
"Social Media Assets","Paid Social","IMAGE","JPG","1080x1080","EN-UK","1"
|
||||
"Social Media Assets","Paid Social","IMAGE","JPG","1080x1920","EN-UK","1"
|
||||
"Social Media Assets","Paid Social","IMAGE","JPG","1080x1080","DE-DE","1"
|
||||
```
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
### Processing Times
|
||||
- **Small Documents** (1-5 pages): 1-2 minutes
|
||||
- **Medium Documents** (6-20 pages): 2-4 minutes
|
||||
- **Large Documents** (20+ pages): 3-6 minutes
|
||||
- **Parallel Advantage**: 3x faster than sequential model processing
|
||||
|
||||
### Accuracy Benefits
|
||||
- **Multi-Model Consensus**: Higher confidence through diverse AI perspectives
|
||||
- **Reduced Blind Spots**: Different models catch different deliverable types
|
||||
- **Quality Enhancement**: Best specifications from multiple analyses
|
||||
- **Comprehensive Coverage**: Bias toward completeness prevents missed assets
|
||||
|
||||
## 💰 Cost Management
|
||||
|
||||
### Pricing Awareness
|
||||
- **OpenAI GPT-5**: $2.50-$10.00 per 1M tokens
|
||||
- **Anthropic Claude**: $3.00-$75.00 per 1M tokens
|
||||
- **Google Gemini**: $1.25-$5.00 per 1M tokens
|
||||
|
||||
### Cost Controls
|
||||
- **Pre-Processing Estimates**: Know costs before execution
|
||||
- **Budget Limits**: Configurable maximum spending per document
|
||||
- **Real-Time Tracking**: Monitor costs during processing
|
||||
- **Model Selection**: Choose quality vs cost balance
|
||||
|
||||
### Typical Costs
|
||||
- **Small Brief**: $0.50-$2.00 (3-model analysis)
|
||||
- **Medium Brief**: $1.00-$5.00 (3-model analysis)
|
||||
- **Large Brief**: $2.00-$10.00 (3-model analysis)
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Model Selection Strategies
|
||||
|
||||
**Maximum Quality (Highest Cost):**
|
||||
```bash
|
||||
--primary-models openai-gpt5,anthropic-opus4,google-gemini25 \
|
||||
--consolidation-model anthropic-opus4
|
||||
```
|
||||
|
||||
**Balanced Performance (Recommended):**
|
||||
```bash
|
||||
--primary-models openai-gpt5,anthropic-sonnet4,google-gemini25 \
|
||||
--consolidation-model openai-gpt5
|
||||
```
|
||||
|
||||
**Cost-Optimized:**
|
||||
```bash
|
||||
--primary-models openai-gpt5,google-gemini25 \
|
||||
--consolidation-model google-gemini25
|
||||
```
|
||||
|
||||
**Speed-Focused:**
|
||||
```bash
|
||||
--primary-models anthropic-sonnet4,google-gemini25 \
|
||||
--consolidation-model anthropic-sonnet4
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# API Configuration
|
||||
OPENAI_API_KEY=your-openai-key
|
||||
ANTHROPIC_API_KEY=your-anthropic-key
|
||||
GOOGLE_API_KEY=your-google-key
|
||||
LLAMACLOUD_API_KEY=your-llamacloud-key
|
||||
|
||||
# Model Settings
|
||||
OPENAI_REASONING_EFFORT=medium
|
||||
ANTHROPIC_MAX_TOKENS=64000
|
||||
GOOGLE_MAX_OUTPUT_TOKENS=100000
|
||||
|
||||
# Processing Controls
|
||||
MINIMUM_SUCCESS_THRESHOLD=1
|
||||
ENABLE_COST_ESTIMATION=true
|
||||
MAX_PROCESSING_COST_USD=10.00
|
||||
```
|
||||
|
||||
## 📖 Advanced Usage
|
||||
|
||||
### Multiplier Field Optimization
|
||||
The system uses **2 multiplier fields** to control expansion:
|
||||
- **`technical_specifications`**: Dimensions, sizes, formats
|
||||
- **`language_country_market`**: Market targeting with ISO codes
|
||||
|
||||
**Example Multiplier Logic:**
|
||||
```
|
||||
Base Deliverable: "Social Media Assets"
|
||||
Technical Specs: ["1080x1080", "1080x1920", "1200x1200"] (3 sizes)
|
||||
Markets: ["EN-UK", "DE-DE", "FR-FR"] (3 markets)
|
||||
Result: 3 × 3 = 9 individual deliverables
|
||||
```
|
||||
|
||||
### Quantity Validation
|
||||
The system uses the `quantity` field as a sense-check:
|
||||
- **Purpose**: Validate that multiplier expansion matches expected count
|
||||
- **Example**: If brief says "20 banners", ensure specs × markets ≈ 20
|
||||
- **Benefit**: Prevents over-multiplication and unrealistic deliverable counts
|
||||
|
||||
### Intelligent Consolidation
|
||||
**"Include if Any Model Found It" Philosophy:**
|
||||
- Multiple models may find different deliverables from the same document
|
||||
- Consolidation includes all legitimate unique deliverables
|
||||
- Smart deduplication prevents true duplicates
|
||||
- Quality enhancement uses best specifications from any model
|
||||
|
||||
## 🛠 Development
|
||||
|
||||
### Architecture
|
||||
- **`llm_service/`**: Multi-provider abstraction layer with async support
|
||||
- **`prompts/`**: External prompt templates and universal schema
|
||||
- **`consolidation_processor.py`**: Multi-model result merging logic
|
||||
- **`config.py`**: Environment variable management and validation
|
||||
|
||||
### Adding New Providers
|
||||
1. Extend `BaseLLMProvider` in `llm_service/`
|
||||
2. Implement async `generate_response()` method
|
||||
3. Add configuration to `.env` and `config.py`
|
||||
4. Update model mappings and CLI interface
|
||||
|
||||
### Customizing Extraction
|
||||
- **Schema**: Edit `prompts/universal_schema.json`
|
||||
- **Prompts**: Modify files in `prompts/` directory
|
||||
- **Processing**: Adjust `expand_deliverables()` function
|
||||
- **Consolidation**: Update consolidation logic
|
||||
|
||||
## 📞 Support & Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
- **API Key Errors**: Verify all keys are set in `.env`
|
||||
- **High Costs**: Adjust model selection or cost limits
|
||||
- **Model Failures**: Check logs for provider-specific errors
|
||||
- **Over-Expansion**: Review multiplier arrays and quantity validation
|
||||
|
||||
### Debug Information
|
||||
- **Processing Logs**: `processing.log` with detailed execution info
|
||||
- **Cost Tracking**: Token usage and cost breakdown per model
|
||||
- **Expansion Details**: Multiplier calculations and validation results
|
||||
- **Model Performance**: Success rates and deliverable count comparisons
|
||||
|
||||
### Getting Help
|
||||
- **Documentation**: See `CLAUDE.md` for detailed technical information
|
||||
- **Logs**: Check `processing.log` for comprehensive debugging information
|
||||
- **Configuration**: Verify `.env` settings and API key validity
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
### Marketing Agencies
|
||||
- **Client Brief Analysis**: Extract all deliverables from complex campaign briefs
|
||||
- **Project Planning**: Generate comprehensive asset lists for timeline planning
|
||||
- **Resource Estimation**: Understand scope and effort required for campaigns
|
||||
|
||||
### Creative Teams
|
||||
- **Asset Inventory**: Catalog all required creative deliverables
|
||||
- **Specification Tracking**: Maintain exact technical requirements
|
||||
- **Multi-Market Campaigns**: Handle localization and market-specific variations
|
||||
|
||||
### Project Managers
|
||||
- **Scope Definition**: Clear deliverable counts and specifications
|
||||
- **Timeline Planning**: Review dates and launch schedules
|
||||
- **Quality Control**: Standardized asset information across projects
|
||||
|
||||
---
|
||||
|
||||
**Enhanced Brief Processing System** - Transforming document analysis through multi-model AI intelligence.
|
||||
220
README_GUI.md
220
README_GUI.md
|
|
@ -1,220 +0,0 @@
|
|||
# Brief Extractor GUI
|
||||
|
||||
Modern web interface for the Enhanced Brief Processing System with real-time progress tracking, multi-model AI processing, and Microsoft SSO authentication.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Development Mode
|
||||
|
||||
1. **Install Dependencies**:
|
||||
```bash
|
||||
# Backend
|
||||
pip install -r server_requirements.txt
|
||||
|
||||
# Frontend
|
||||
cd frontend
|
||||
npm install
|
||||
cd ..
|
||||
```
|
||||
|
||||
2. **Configure Environment**:
|
||||
```bash
|
||||
# Copy and edit .env file with your API keys
|
||||
# DEV_MODE=true is already set for local development
|
||||
```
|
||||
|
||||
3. **Start Services**:
|
||||
```bash
|
||||
# Terminal 1: Backend
|
||||
python run_server.py
|
||||
|
||||
# Terminal 2: Frontend
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. **Access Application**:
|
||||
- Frontend: http://localhost:3000
|
||||
- Backend API: http://localhost:8000
|
||||
- Health Check: http://localhost:8000/health
|
||||
|
||||
### Production Deployment
|
||||
|
||||
1. **Configure Authentication**:
|
||||
```bash
|
||||
# Update .env with your Microsoft Azure AD app registration:
|
||||
MSAL_CLIENT_ID=your-client-id
|
||||
MSAL_CLIENT_SECRET=your-client-secret
|
||||
MSAL_TENANT_ID=your-tenant-id
|
||||
DEV_MODE=false
|
||||
```
|
||||
|
||||
2. **Deploy with Docker**:
|
||||
```bash
|
||||
# Build and start all services
|
||||
docker-compose up -d
|
||||
|
||||
# Check status
|
||||
docker-compose ps
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend (Python/Quart)
|
||||
- **Async web framework** with WebSocket support
|
||||
- **Job queue system** with concurrent processing
|
||||
- **Multi-model AI integration** preserving existing processing logic
|
||||
- **Real-time progress tracking** via WebSocket
|
||||
- **MSAL authentication** with dev mode bypass
|
||||
- **File management** with auto-cleanup
|
||||
|
||||
### Frontend (React/TypeScript)
|
||||
- **Modern React** with TypeScript and Vite
|
||||
- **Real-time updates** via WebSocket
|
||||
- **MSAL integration** for SSO authentication
|
||||
- **Model configuration** interface
|
||||
- **Progress visualization** with provider status
|
||||
- **Responsive design** with Tailwind CSS
|
||||
|
||||
### Key Features
|
||||
✅ **Multi-file upload** with drag-and-drop
|
||||
✅ **Real-time progress** for all processing stages
|
||||
✅ **Model selection** (primary + consolidation models)
|
||||
✅ **Cost estimation** before processing
|
||||
✅ **Live log streaming** for active jobs
|
||||
✅ **Batch operations** (multi-upload, batch download)
|
||||
✅ **Authentication** (MSAL SSO + dev mode)
|
||||
✅ **Auto-cleanup** of processed files
|
||||
✅ **Docker deployment** ready
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
- `GET /api/auth/config` - Get MSAL configuration
|
||||
- `POST /api/auth/validate` - Validate access token
|
||||
- `GET /api/auth/user` - Get current user info
|
||||
- `POST /api/auth/logout` - Get logout URL
|
||||
|
||||
### Jobs
|
||||
- `POST /api/jobs` - Upload files and create jobs
|
||||
- `GET /api/jobs` - List user's jobs
|
||||
- `GET /api/jobs/<id>` - Get job details
|
||||
- `GET /api/jobs/<id>/download` - Download CSV result
|
||||
- `GET /api/jobs/<id>/logs` - Get job logs
|
||||
- `DELETE /api/jobs/<id>` - Delete job
|
||||
- `POST /api/jobs/batch-download` - Download multiple CSVs as ZIP
|
||||
|
||||
### Configuration
|
||||
- `GET /api/config/models` - Available models with pricing
|
||||
- `GET /api/config/defaults` - Default configuration
|
||||
- `POST /api/config/estimate` - Estimate processing cost
|
||||
- `POST /api/config/validate` - Validate model configuration
|
||||
|
||||
### System
|
||||
- `GET /health` - Health check
|
||||
- `GET /api/config/system` - System information
|
||||
|
||||
## WebSocket Events
|
||||
|
||||
### Job Lifecycle
|
||||
- `queue.snapshot` - Initial job list
|
||||
- `job.created` - New job added to queue
|
||||
- `job.progress` - Progress update
|
||||
- `job.provider_update` - Individual model status
|
||||
- `job.log` - Real-time log entry
|
||||
- `job.completed` - Job finished successfully
|
||||
- `job.failed` - Job failed with error
|
||||
- `job.deleted` - Job removed
|
||||
|
||||
## Environment Variables
|
||||
|
||||
See `.env` file for complete configuration. Key settings:
|
||||
|
||||
```bash
|
||||
# Development
|
||||
DEV_MODE=true # Bypass MSAL in development
|
||||
ALLOWED_ORIGINS=http://localhost:3000
|
||||
|
||||
# Authentication (Production)
|
||||
MSAL_CLIENT_ID=your-client-id
|
||||
MSAL_CLIENT_SECRET=your-secret
|
||||
MSAL_TENANT_ID=your-tenant-id
|
||||
|
||||
# Performance
|
||||
MAX_CONCURRENT_JOBS=2
|
||||
MAX_UPLOAD_SIZE_MB=200
|
||||
FILE_RETENTION_HOURS=24
|
||||
|
||||
# All existing AI model API keys remain the same
|
||||
```
|
||||
|
||||
## CLI Compatibility
|
||||
|
||||
The original CLI interface remains fully functional:
|
||||
|
||||
```bash
|
||||
# Original usage still works
|
||||
python core/process_brief_enhanced.py document.pdf
|
||||
|
||||
# With custom models
|
||||
python core/process_brief_enhanced.py document.pdf \
|
||||
--primary-models openai-gpt5,anthropic-sonnet4 \
|
||||
--consolidation-model anthropic-opus4
|
||||
```
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
├── server/ # Quart backend
|
||||
│ ├── api/ # REST endpoints
|
||||
│ ├── auth/ # MSAL authentication
|
||||
│ ├── jobs/ # Job management
|
||||
│ ├── ws/ # WebSocket handling
|
||||
│ └── runners/ # Job processing
|
||||
├── core/ # Original processing logic (moved)
|
||||
├── frontend/ # React frontend
|
||||
├── prompts/ # AI prompts and schemas
|
||||
└── docker-compose.yml # Container orchestration
|
||||
```
|
||||
|
||||
### Adding Features
|
||||
- **New API endpoints**: Add to `server/api/`
|
||||
- **WebSocket events**: Extend `ws/manager.py`
|
||||
- **UI components**: Add to `frontend/src/components/`
|
||||
- **Processing logic**: Modify `core/` modules
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Backend tests
|
||||
cd server
|
||||
python -m pytest
|
||||
|
||||
# Frontend tests
|
||||
cd frontend
|
||||
npm test
|
||||
|
||||
# Integration tests
|
||||
docker-compose -f docker-compose.test.yml up --abort-on-container-exit
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
- [ ] Configure Microsoft Azure AD app registration
|
||||
- [ ] Set production environment variables
|
||||
- [ ] Configure HTTPS certificates
|
||||
- [ ] Set up monitoring and logging
|
||||
- [ ] Configure backup for processed files
|
||||
- [ ] Set up alerts for processing failures
|
||||
- [ ] Load test with expected file volumes
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check logs: `docker-compose logs`
|
||||
2. Health check: Visit `/health` endpoint
|
||||
3. WebSocket status: Check browser dev tools
|
||||
4. File permissions: Ensure `server/data/` is writable
|
||||
|
||||
The GUI provides the same powerful multi-model AI processing as the CLI with an intuitive web interface, real-time progress tracking, and enterprise authentication.
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comparison script between original and enhanced systems
|
||||
Shows the dramatic improvements in asset extraction
|
||||
"""
|
||||
|
||||
import csv
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def count_assets_in_csv(csv_path):
|
||||
"""Count non-empty assets in CSV file"""
|
||||
if not os.path.exists(csv_path):
|
||||
return 0
|
||||
|
||||
try:
|
||||
with open(csv_path, 'r', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
rows = list(reader)
|
||||
# Count rows with actual content (not empty title)
|
||||
return len([row for row in rows if row.get('title', '').strip()])
|
||||
except:
|
||||
return 0
|
||||
|
||||
def run_processor(script, file_path, description):
|
||||
"""Run a processor and return results"""
|
||||
python_executable = '/Users/daveporter/Desktop/CODING-2024/ADIDAS-TEST-MULTIPASS/adi-gem-brief/bin/python'
|
||||
|
||||
print(f"\n🔬 Testing {description}")
|
||||
print(f" File: {os.path.basename(file_path)}")
|
||||
print(f" Script: {script}")
|
||||
|
||||
try:
|
||||
result = subprocess.run([
|
||||
python_executable,
|
||||
script,
|
||||
file_path
|
||||
], capture_output=True, text=True, timeout=300)
|
||||
|
||||
# Extract output filename
|
||||
output_file = None
|
||||
for line in result.stdout.split('\n'):
|
||||
if '__FILENAME__:' in line:
|
||||
output_file = line.split('__FILENAME__:')[1].strip()
|
||||
break
|
||||
|
||||
if output_file and os.path.exists(output_file):
|
||||
asset_count = count_assets_in_csv(output_file)
|
||||
print(f" ✅ Success: {asset_count} assets extracted")
|
||||
return {
|
||||
'success': True,
|
||||
'asset_count': asset_count,
|
||||
'output_file': output_file
|
||||
}
|
||||
else:
|
||||
print(f" ❌ Failed: No output file generated")
|
||||
return {'success': False, 'asset_count': 0}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f" ⏰ Timeout: Processing took too long")
|
||||
return {'success': False, 'asset_count': 0}
|
||||
except Exception as e:
|
||||
print(f" 💥 Error: {str(e)}")
|
||||
return {'success': False, 'asset_count': 0}
|
||||
|
||||
def main():
|
||||
"""Compare original vs enhanced systems"""
|
||||
print("🎯 ADIDAS BRIEF PROCESSING SYSTEM COMPARISON")
|
||||
print("=" * 60)
|
||||
|
||||
# Test files to compare
|
||||
test_files = [
|
||||
"BRIEFS_TO_TEST/5653728 - CAM_TR_RETAIL_DigitalScreens_YouGotThis_SS25_Local.pdf",
|
||||
"BRIEFS_TO_TEST/15.05_Trail Maker_SS25_Brief_Oliver.pptx",
|
||||
"BRIEFS_TO_TEST/5342242 - CAM_EMS_CRM_GlobalAdapt_SALE_ValentinesDay_SS25.docx",
|
||||
]
|
||||
|
||||
# Check if files exist
|
||||
available_files = []
|
||||
for file_path in test_files:
|
||||
if os.path.exists(file_path):
|
||||
available_files.append(file_path)
|
||||
else:
|
||||
print(f"⚠️ Test file not found: {file_path}")
|
||||
|
||||
if not available_files:
|
||||
print("❌ No test files available")
|
||||
return
|
||||
|
||||
print(f"📂 Testing with {len(available_files)} files")
|
||||
|
||||
results = {}
|
||||
|
||||
for file_path in available_files:
|
||||
file_name = os.path.basename(file_path)
|
||||
results[file_name] = {}
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"📄 TESTING: {file_name}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Test original system
|
||||
if os.path.exists('process_brief.py'):
|
||||
original_result = run_processor('process_brief.py', file_path, "Original System")
|
||||
results[file_name]['original'] = original_result
|
||||
else:
|
||||
print("\n⚠️ Original system not found, skipping")
|
||||
results[file_name]['original'] = {'success': False, 'asset_count': 0}
|
||||
|
||||
# Test enhanced system
|
||||
if os.path.exists('process_brief_enhanced.py'):
|
||||
enhanced_result = run_processor('process_brief_enhanced.py', file_path, "Enhanced System")
|
||||
results[file_name]['enhanced'] = enhanced_result
|
||||
else:
|
||||
print("\n❌ Enhanced system not found")
|
||||
results[file_name]['enhanced'] = {'success': False, 'asset_count': 0}
|
||||
|
||||
# Generate comparison report
|
||||
print(f"\n\n📊 COMPARISON RESULTS")
|
||||
print("=" * 80)
|
||||
|
||||
total_original = 0
|
||||
total_enhanced = 0
|
||||
successful_comparisons = 0
|
||||
|
||||
print(f"{'File':<40} {'Original':<12} {'Enhanced':<12} {'Improvement':<12}")
|
||||
print("-" * 80)
|
||||
|
||||
for file_name, results_data in results.items():
|
||||
original_count = results_data['original']['asset_count']
|
||||
enhanced_count = results_data['enhanced']['asset_count']
|
||||
|
||||
if results_data['original']['success'] and results_data['enhanced']['success']:
|
||||
improvement = f"+{enhanced_count - original_count}" if enhanced_count > original_count else str(enhanced_count - original_count)
|
||||
improvement_pct = f"({((enhanced_count - original_count) / max(original_count, 1)) * 100:.0f}%)"
|
||||
total_original += original_count
|
||||
total_enhanced += enhanced_count
|
||||
successful_comparisons += 1
|
||||
else:
|
||||
improvement = "N/A"
|
||||
improvement_pct = ""
|
||||
|
||||
# Truncate filename if too long
|
||||
display_name = file_name if len(file_name) <= 35 else file_name[:32] + "..."
|
||||
|
||||
print(f"{display_name:<40} {original_count:<12} {enhanced_count:<12} {improvement} {improvement_pct}")
|
||||
|
||||
print("-" * 80)
|
||||
|
||||
if successful_comparisons > 0:
|
||||
overall_improvement = total_enhanced - total_original
|
||||
overall_improvement_pct = ((total_enhanced - total_original) / max(total_original, 1)) * 100
|
||||
|
||||
print(f"{'TOTALS':<40} {total_original:<12} {total_enhanced:<12} {'+' if overall_improvement >= 0 else ''}{overall_improvement} ({overall_improvement_pct:.0f}%)")
|
||||
|
||||
print(f"\n🎉 SUMMARY:")
|
||||
print(f" • Files successfully processed: {successful_comparisons}")
|
||||
print(f" • Total assets (Original): {total_original}")
|
||||
print(f" • Total assets (Enhanced): {total_enhanced}")
|
||||
print(f" • Overall improvement: +{overall_improvement} assets ({overall_improvement_pct:.0f}%)")
|
||||
print(f" • Average assets per file (Original): {total_original/successful_comparisons:.1f}")
|
||||
print(f" • Average assets per file (Enhanced): {total_enhanced/successful_comparisons:.1f}")
|
||||
|
||||
if overall_improvement_pct > 50:
|
||||
print(f"\n🏆 OUTSTANDING IMPROVEMENT: +{overall_improvement_pct:.0f}% more assets extracted!")
|
||||
elif overall_improvement_pct > 20:
|
||||
print(f"\n✨ SIGNIFICANT IMPROVEMENT: +{overall_improvement_pct:.0f}% more assets extracted!")
|
||||
elif overall_improvement_pct > 0:
|
||||
print(f"\n👍 GOOD IMPROVEMENT: +{overall_improvement_pct:.0f}% more assets extracted!")
|
||||
else:
|
||||
print("❌ No successful comparisons could be made")
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print("🎯 Enhanced system demonstrates significant improvements in:")
|
||||
print(" • Asset extraction completeness")
|
||||
print(" • Document structure understanding")
|
||||
print(" • Multi-format compatibility")
|
||||
print(" • Validation and quality assurance")
|
||||
print(" • Detailed technical specifications")
|
||||
print(" • Creative direction capture")
|
||||
print(f"{'='*80}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
148
core/config.py
148
core/config.py
|
|
@ -1,148 +0,0 @@
|
|||
"""
|
||||
Configuration management for Enhanced Brief Processing System
|
||||
Loads environment variables and provides configuration validation
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
class Config:
|
||||
"""Centralized configuration management"""
|
||||
|
||||
# API Keys
|
||||
OPENAI_API_KEY: str = os.getenv('OPENAI_API_KEY', '')
|
||||
ANTHROPIC_API_KEY: str = os.getenv('ANTHROPIC_API_KEY', '')
|
||||
GOOGLE_API_KEY: str = os.getenv('GOOGLE_API_KEY', '')
|
||||
LLAMACLOUD_API_KEY: str = os.getenv('LLAMACLOUD_API_KEY', '')
|
||||
|
||||
# OpenAI Configuration
|
||||
OPENAI_MODEL: str = os.getenv('OPENAI_MODEL', 'gpt-5.1')
|
||||
OPENAI_REASONING_EFFORT: str = os.getenv('OPENAI_REASONING_EFFORT', 'medium')
|
||||
OPENAI_TIMEOUT: int = int(os.getenv('OPENAI_TIMEOUT', '3600'))
|
||||
OPENAI_MAX_RETRIES: int = int(os.getenv('OPENAI_MAX_RETRIES', '2'))
|
||||
|
||||
# Google Configuration
|
||||
GOOGLE_MODEL: str = os.getenv('GOOGLE_MODEL', 'gemini-3.1-pro-preview')
|
||||
GOOGLE_TEMPERATURE: float = float(os.getenv('GOOGLE_TEMPERATURE', '0.1'))
|
||||
GOOGLE_MAX_OUTPUT_TOKENS: int = int(os.getenv('GOOGLE_MAX_OUTPUT_TOKENS', '8192'))
|
||||
GOOGLE_THINKING_BUDGET: int = int(os.getenv('GOOGLE_THINKING_BUDGET', '12000'))
|
||||
GOOGLE_TIMEOUT: int = int(os.getenv('GOOGLE_TIMEOUT', '300'))
|
||||
|
||||
# Anthropic Configuration
|
||||
ANTHROPIC_MODEL_OPUS: str = os.getenv('ANTHROPIC_MODEL_OPUS', 'claude-opus-4-5-20251101')
|
||||
ANTHROPIC_MODEL_SONNET: str = os.getenv('ANTHROPIC_MODEL_SONNET', 'claude-sonnet-4-5-20250929')
|
||||
ANTHROPIC_TEMPERATURE: float = float(os.getenv('ANTHROPIC_TEMPERATURE', '0.1'))
|
||||
ANTHROPIC_MAX_TOKENS: int = int(os.getenv('ANTHROPIC_MAX_TOKENS', '32000'))
|
||||
ANTHROPIC_THINKING_BUDGET: int = int(os.getenv('ANTHROPIC_THINKING_BUDGET', '12000'))
|
||||
ANTHROPIC_TIMEOUT: int = int(os.getenv('ANTHROPIC_TIMEOUT', '300'))
|
||||
|
||||
# Processing Configuration
|
||||
DEFAULT_PRIMARY_MODELS: str = os.getenv('DEFAULT_PRIMARY_MODELS', 'openai-gpt51,anthropic-sonnet45,google-gemini31')
|
||||
DEFAULT_CONSOLIDATION_MODEL: str = os.getenv('DEFAULT_CONSOLIDATION_MODEL', 'openai-gpt51')
|
||||
MINIMUM_SUCCESS_THRESHOLD: int = int(os.getenv('MINIMUM_SUCCESS_THRESHOLD', '1'))
|
||||
ENABLE_COST_ESTIMATION: bool = os.getenv('ENABLE_COST_ESTIMATION', 'true').lower() == 'true'
|
||||
MAX_PROCESSING_COST_USD: float = float(os.getenv('MAX_PROCESSING_COST_USD', '10.00'))
|
||||
|
||||
# Model Pricing (per 1M tokens)
|
||||
PRICING = {
|
||||
'openai-gpt51': {
|
||||
'input': 1.25,
|
||||
'cached_input': 0.625,
|
||||
'output': 10.00
|
||||
},
|
||||
'anthropic-opus45': {
|
||||
'input': 5.00,
|
||||
'output': 25.00
|
||||
},
|
||||
'anthropic-sonnet45': {
|
||||
'input': 3.00,
|
||||
'output': 15.00
|
||||
},
|
||||
'google-gemini31': {
|
||||
'input': 1.25,
|
||||
'output': 5.00
|
||||
}
|
||||
}
|
||||
|
||||
# Model mappings for CLI compatibility
|
||||
MODEL_MAPPINGS = {
|
||||
'openai-gpt51': ('openai', OPENAI_MODEL),
|
||||
'anthropic-opus45': ('anthropic', ANTHROPIC_MODEL_OPUS),
|
||||
'anthropic-sonnet45': ('anthropic', ANTHROPIC_MODEL_SONNET),
|
||||
'google-gemini31': ('google', GOOGLE_MODEL)
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def validate_api_keys(cls) -> Dict[str, bool]:
|
||||
"""Validate that required API keys are set"""
|
||||
return {
|
||||
'openai': bool(cls.OPENAI_API_KEY and cls.OPENAI_API_KEY != 'your-openai-api-key-here'),
|
||||
'anthropic': bool(cls.ANTHROPIC_API_KEY and cls.ANTHROPIC_API_KEY != 'your-anthropic-api-key-here'),
|
||||
'google': bool(cls.GOOGLE_API_KEY and cls.GOOGLE_API_KEY != 'your-google-api-key-here'),
|
||||
'llamacloud': bool(cls.LLAMACLOUD_API_KEY and cls.LLAMACLOUD_API_KEY != 'your-llamacloud-api-key-here')
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_provider_config(cls, provider: str) -> Dict[str, Any]:
|
||||
"""Get configuration for a specific provider"""
|
||||
if provider == 'openai':
|
||||
return {
|
||||
'api_key': cls.OPENAI_API_KEY,
|
||||
'model': cls.OPENAI_MODEL,
|
||||
'reasoning_effort': cls.OPENAI_REASONING_EFFORT,
|
||||
'timeout': cls.OPENAI_TIMEOUT,
|
||||
'max_retries': cls.OPENAI_MAX_RETRIES
|
||||
}
|
||||
elif provider == 'google':
|
||||
return {
|
||||
'api_key': cls.GOOGLE_API_KEY,
|
||||
'model': cls.GOOGLE_MODEL,
|
||||
'temperature': cls.GOOGLE_TEMPERATURE,
|
||||
'max_output_tokens': cls.GOOGLE_MAX_OUTPUT_TOKENS,
|
||||
'thinking_budget': cls.GOOGLE_THINKING_BUDGET,
|
||||
'timeout': cls.GOOGLE_TIMEOUT
|
||||
}
|
||||
elif provider == 'anthropic':
|
||||
return {
|
||||
'api_key': cls.ANTHROPIC_API_KEY,
|
||||
'model_opus': cls.ANTHROPIC_MODEL_OPUS,
|
||||
'model_sonnet': cls.ANTHROPIC_MODEL_SONNET,
|
||||
'temperature': cls.ANTHROPIC_TEMPERATURE,
|
||||
'max_tokens': cls.ANTHROPIC_MAX_TOKENS,
|
||||
'thinking_budget': cls.ANTHROPIC_THINKING_BUDGET,
|
||||
'timeout': cls.ANTHROPIC_TIMEOUT
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Unknown provider: {provider}")
|
||||
|
||||
@classmethod
|
||||
def get_default_primary_models(cls) -> List[str]:
|
||||
"""Get default list of primary analysis models"""
|
||||
return cls.DEFAULT_PRIMARY_MODELS.split(',')
|
||||
|
||||
@classmethod
|
||||
def get_model_info(cls, model_key: str) -> tuple:
|
||||
"""Get provider and model name for a model key"""
|
||||
if model_key not in cls.MODEL_MAPPINGS:
|
||||
raise ValueError(f"Unknown model key: {model_key}. Available: {list(cls.MODEL_MAPPINGS.keys())}")
|
||||
return cls.MODEL_MAPPINGS[model_key]
|
||||
|
||||
@classmethod
|
||||
def estimate_cost(cls, model_key: str, input_tokens: int, output_tokens: int, cached_tokens: int = 0) -> float:
|
||||
"""Estimate processing cost for a model"""
|
||||
if model_key not in cls.PRICING:
|
||||
return 0.0
|
||||
|
||||
pricing = cls.PRICING[model_key]
|
||||
input_cost = (input_tokens / 1_000_000) * pricing['input']
|
||||
output_cost = (output_tokens / 1_000_000) * pricing['output']
|
||||
cached_cost = (cached_tokens / 1_000_000) * pricing.get('cached_input', pricing['input'])
|
||||
|
||||
return input_cost + output_cost + cached_cost
|
||||
|
||||
# Global config instance
|
||||
config = Config()
|
||||
|
|
@ -1,353 +0,0 @@
|
|||
"""
|
||||
Consolidation processor for merging multiple LLM analysis results
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Any, Tuple
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
|
||||
from .llm_service import ProviderManager, LLMResponse
|
||||
from .config import config
|
||||
|
||||
@dataclass
|
||||
class ConsolidationResult:
|
||||
"""Result of consolidation process"""
|
||||
consolidated_deliverables: List[Any] # BaseDeliverable
|
||||
expanded_assets: List[Any] # MarketingAsset
|
||||
consolidation_metadata: Dict[str, Any]
|
||||
warnings: List[str]
|
||||
|
||||
class ConsolidationProcessor:
|
||||
"""Processes multiple LLM analysis results into a single consolidated output"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.provider_manager = ProviderManager()
|
||||
|
||||
async def consolidate_results(
|
||||
self,
|
||||
analysis_responses: List[LLMResponse],
|
||||
consolidation_model: str,
|
||||
document_content: str = ""
|
||||
) -> ConsolidationResult:
|
||||
"""
|
||||
Consolidate multiple analysis results using the specified consolidation model
|
||||
|
||||
Args:
|
||||
analysis_responses: List of LLM responses from primary analysis
|
||||
consolidation_model: Model key for consolidation (e.g., 'anthropic-opus45')
|
||||
document_content: Optional original document content for context
|
||||
|
||||
Returns:
|
||||
ConsolidationResult with final consolidated deliverables
|
||||
"""
|
||||
self.logger.info(f"Starting consolidation with {len(analysis_responses)} model results using {consolidation_model}")
|
||||
|
||||
# Log individual model deliverable counts
|
||||
successful_models = []
|
||||
deliverable_counts = []
|
||||
for i, response in enumerate(analysis_responses):
|
||||
if response.success:
|
||||
count = self._count_deliverables_in_response(response.content)
|
||||
deliverable_counts.append(count)
|
||||
successful_models.append(f"{response.provider} {response.model_used}")
|
||||
self.logger.info(f"Model {i+1} ({response.provider} {response.model_used}): {count} base deliverables")
|
||||
|
||||
if deliverable_counts:
|
||||
avg_deliverables = sum(deliverable_counts) / len(deliverable_counts)
|
||||
self.logger.info(f"Average deliverables across {len(deliverable_counts)} models: {avg_deliverables:.1f}")
|
||||
else:
|
||||
self.logger.warning("No successful model responses to analyze")
|
||||
|
||||
# Extract and format results from all models
|
||||
formatted_results = self._format_model_results(analysis_responses)
|
||||
|
||||
# Prepare consolidation prompt
|
||||
consolidation_prompt = await self._prepare_consolidation_prompt(formatted_results)
|
||||
|
||||
# Load system message for consolidation
|
||||
system_message = self._load_consolidation_system_prompt()
|
||||
|
||||
# Execute consolidation using specified model
|
||||
try:
|
||||
provider = self.provider_manager.get_provider(consolidation_model)
|
||||
messages = provider.prepare_messages(system_message, consolidation_prompt)
|
||||
|
||||
# Use the universal base deliverable schema for structured output
|
||||
from .process_brief_enhanced import UNIVERSAL_BASE_DELIVERABLE_SCHEMA
|
||||
|
||||
consolidation_response = await provider.generate_response(
|
||||
messages=messages,
|
||||
schema=UNIVERSAL_BASE_DELIVERABLE_SCHEMA
|
||||
)
|
||||
|
||||
if not consolidation_response.success:
|
||||
raise Exception(f"Consolidation failed: {consolidation_response.error}")
|
||||
|
||||
# Parse the consolidated results - import here to avoid circular import
|
||||
from .process_brief_enhanced import BaseDeliverable, expand_deliverables
|
||||
|
||||
try:
|
||||
consolidated_data = json.loads(consolidation_response.content)
|
||||
|
||||
if 'assets' not in consolidated_data:
|
||||
# PROBLEM DETECTED - Log everything verbosely
|
||||
self.logger.error(f"[CONSOLIDATION] ========== MISSING 'assets' KEY - VERBOSE DEBUG ==========")
|
||||
self.logger.error(f"[CONSOLIDATION] Model: {consolidation_model}")
|
||||
self.logger.error(f"[CONSOLIDATION] Response success: {consolidation_response.success}")
|
||||
self.logger.error(f"[CONSOLIDATION] Response content length: {len(consolidation_response.content)} chars")
|
||||
self.logger.error(f"[CONSOLIDATION] Response content type: {type(consolidation_response.content)}")
|
||||
self.logger.error(f"[CONSOLIDATION] Full raw content: {consolidation_response.content}")
|
||||
self.logger.error(f"[CONSOLIDATION] Parsed data type: {type(consolidated_data)}")
|
||||
self.logger.error(f"[CONSOLIDATION] Parsed data keys: {list(consolidated_data.keys()) if isinstance(consolidated_data, dict) else 'N/A'}")
|
||||
self.logger.error(f"[CONSOLIDATION] Full parsed data: {consolidated_data}")
|
||||
|
||||
# Save debug file
|
||||
self._save_consolidation_debug(consolidation_response, consolidated_data, analysis_responses)
|
||||
raise KeyError("Response missing 'assets' key")
|
||||
|
||||
# SUCCESS - Just log summary
|
||||
self.logger.info(f"Consolidation completed: {len(consolidated_data['assets'])} base deliverables")
|
||||
|
||||
base_deliverables = [BaseDeliverable(**item) for item in consolidated_data['assets']]
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"[CONSOLIDATION] ========== JSON PARSE ERROR ==========")
|
||||
self.logger.error(f"[CONSOLIDATION] Parse error: {e}")
|
||||
self.logger.error(f"[CONSOLIDATION] Full response content: {consolidation_response.content}")
|
||||
raise
|
||||
except KeyError as e:
|
||||
# Already logged in detail above
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"[CONSOLIDATION] Error processing consolidation response: {e}")
|
||||
self.logger.error(f"[CONSOLIDATION] Full response content: {consolidation_response.content}")
|
||||
raise
|
||||
|
||||
# Expand consolidated base deliverables into individual assets
|
||||
expanded_assets, expansion_warnings = expand_deliverables(base_deliverables)
|
||||
self.logger.info(f"Expansion completed: {len(expanded_assets)} individual assets")
|
||||
|
||||
# Create consolidation metadata
|
||||
metadata = self._create_consolidation_metadata(
|
||||
analysis_responses,
|
||||
consolidation_response,
|
||||
base_deliverables,
|
||||
expanded_assets
|
||||
)
|
||||
|
||||
return ConsolidationResult(
|
||||
consolidated_deliverables=base_deliverables,
|
||||
expanded_assets=expanded_assets,
|
||||
consolidation_metadata=metadata,
|
||||
warnings=expansion_warnings
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Consolidation failed: {e}")
|
||||
raise
|
||||
|
||||
def _count_deliverables_in_response(self, content: str) -> int:
|
||||
"""Count the number of deliverables in a model's JSON response"""
|
||||
try:
|
||||
data = json.loads(content)
|
||||
if isinstance(data, dict) and 'assets' in data:
|
||||
return len(data['assets'])
|
||||
return 0
|
||||
except (json.JSONDecodeError, KeyError, TypeError):
|
||||
return 0
|
||||
|
||||
def _format_model_results(self, responses: List[LLMResponse]) -> str:
|
||||
"""Format analysis results from multiple models for consolidation prompt"""
|
||||
formatted_results = []
|
||||
|
||||
for i, response in enumerate(responses):
|
||||
if response.success:
|
||||
model_info = f"**MODEL {i+1}: {response.provider.upper()} {response.model_used}**"
|
||||
|
||||
# Try to extract JSON content
|
||||
try:
|
||||
# Parse the JSON to validate it
|
||||
result_data = json.loads(response.content)
|
||||
formatted_content = json.dumps(result_data, indent=2)
|
||||
except json.JSONDecodeError:
|
||||
# Fallback to raw content if not valid JSON
|
||||
formatted_content = response.content
|
||||
|
||||
formatted_results.append(f"{model_info}\n```json\n{formatted_content}\n```")
|
||||
else:
|
||||
self.logger.warning(f"Skipping failed response from {response.provider} {response.model_used}: {response.error}")
|
||||
|
||||
return "\n\n".join(formatted_results)
|
||||
|
||||
async def _prepare_consolidation_prompt(self, formatted_results: str) -> str:
|
||||
"""Prepare the consolidation prompt with model results"""
|
||||
import asyncio
|
||||
|
||||
def _read_template():
|
||||
"""Blocking template read operation for thread pool"""
|
||||
# Load consolidation prompt template - go up one level from core/ to find prompts/
|
||||
prompt_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'prompts', 'consolidation_analysis.txt')
|
||||
with open(prompt_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
template = await loop.run_in_executor(None, _read_template)
|
||||
return template.format(models_results=formatted_results)
|
||||
|
||||
except FileNotFoundError:
|
||||
self.logger.error("Consolidation prompt template not found")
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error preparing consolidation prompt: {e}")
|
||||
raise
|
||||
|
||||
def _load_consolidation_system_prompt(self) -> str:
|
||||
"""Load system prompt for consolidation"""
|
||||
return """You are an expert data consolidation specialist. Your task is to intelligently merge multiple LLM analysis results into the most complete and accurate dataset possible. Follow the consolidation strategy provided in the user prompt, with emphasis on completeness and thoroughness. Return only valid JSON in the specified format."""
|
||||
|
||||
def _save_consolidation_debug(self, consolidation_response, consolidated_data, analysis_responses):
|
||||
"""Save debug information about failed consolidation"""
|
||||
try:
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
debug_file = os.path.join(tempfile.gettempdir(), f"consolidation_debug_{timestamp}.json")
|
||||
|
||||
debug_info = {
|
||||
"timestamp": timestamp,
|
||||
"consolidation_model": consolidation_response.model_used,
|
||||
"consolidation_provider": consolidation_response.provider,
|
||||
"raw_content": consolidation_response.content,
|
||||
"parsed_data": consolidated_data,
|
||||
"response_success": consolidation_response.success,
|
||||
"response_error": consolidation_response.error,
|
||||
"token_usage": {
|
||||
"input": consolidation_response.token_usage.input_tokens,
|
||||
"output": consolidation_response.token_usage.output_tokens,
|
||||
"total": consolidation_response.token_usage.get_total()
|
||||
},
|
||||
"primary_analysis_results": [
|
||||
{
|
||||
"provider": r.provider,
|
||||
"model": r.model_used,
|
||||
"success": r.success,
|
||||
"deliverable_count": self._count_deliverables_in_response(r.content) if r.success else 0,
|
||||
"content_preview": r.content[:500] if r.success else r.error
|
||||
}
|
||||
for r in analysis_responses
|
||||
]
|
||||
}
|
||||
|
||||
with open(debug_file, 'w') as f:
|
||||
json.dump(debug_info, f, indent=2)
|
||||
|
||||
self.logger.error(f"[CONSOLIDATION] Debug info saved to: {debug_file}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"[CONSOLIDATION] Failed to save debug info: {e}")
|
||||
|
||||
def _create_consolidation_metadata(
|
||||
self,
|
||||
analysis_responses: List[LLMResponse],
|
||||
consolidation_response: LLMResponse,
|
||||
base_deliverables: List[Any],
|
||||
expanded_assets: List[Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Create metadata about the consolidation process"""
|
||||
|
||||
# Analyze model contributions
|
||||
model_stats = {}
|
||||
total_primary_tokens = 0
|
||||
total_primary_cost = 0.0
|
||||
|
||||
for response in analysis_responses:
|
||||
if response.success:
|
||||
model_key = f"{response.provider}_{response.model_used}"
|
||||
model_stats[model_key] = {
|
||||
'tokens_used': response.token_usage.get_total(),
|
||||
'processing_time': response.processing_time,
|
||||
'success': True
|
||||
}
|
||||
total_primary_tokens += response.token_usage.get_total()
|
||||
|
||||
# Estimate cost for this response
|
||||
try:
|
||||
# Find the correct model key for this response
|
||||
provider_model_key = None
|
||||
for key in config.MODEL_MAPPINGS.keys():
|
||||
provider_name, model_name = config.get_model_info(key)
|
||||
if provider_name == response.provider and model_name == response.model_used:
|
||||
provider_model_key = key
|
||||
break
|
||||
|
||||
if provider_model_key:
|
||||
provider = self.provider_manager.get_provider(provider_model_key)
|
||||
cost = provider.estimate_cost(
|
||||
response.token_usage.input_tokens,
|
||||
response.token_usage.output_tokens,
|
||||
response.token_usage.cached_input_tokens
|
||||
)
|
||||
total_primary_cost += cost
|
||||
model_stats[model_key]['estimated_cost'] = cost
|
||||
else:
|
||||
model_stats[model_key]['estimated_cost'] = 0.0
|
||||
except:
|
||||
model_stats[model_key]['estimated_cost'] = 0.0
|
||||
else:
|
||||
model_key = f"{response.provider}_{response.model_used}"
|
||||
model_stats[model_key] = {
|
||||
'tokens_used': 0,
|
||||
'processing_time': response.processing_time,
|
||||
'success': False,
|
||||
'error': response.error,
|
||||
'estimated_cost': 0.0
|
||||
}
|
||||
|
||||
# Consolidation model stats
|
||||
consolidation_cost = 0.0
|
||||
try:
|
||||
# Find the correct model key for consolidation response
|
||||
consolidation_model_key = None
|
||||
for key in config.MODEL_MAPPINGS.keys():
|
||||
provider_name, model_name = config.get_model_info(key)
|
||||
if provider_name == consolidation_response.provider and model_name == consolidation_response.model_used:
|
||||
consolidation_model_key = key
|
||||
break
|
||||
|
||||
if consolidation_model_key:
|
||||
provider = self.provider_manager.get_provider(consolidation_model_key)
|
||||
consolidation_cost = provider.estimate_cost(
|
||||
consolidation_response.token_usage.input_tokens,
|
||||
consolidation_response.token_usage.output_tokens,
|
||||
consolidation_response.token_usage.cached_input_tokens
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
'consolidation_model': consolidation_response.model_used,
|
||||
'consolidation_provider': consolidation_response.provider,
|
||||
'primary_models_used': len([r for r in analysis_responses if r.success]),
|
||||
'total_models_attempted': len(analysis_responses),
|
||||
'base_deliverables_count': len(base_deliverables),
|
||||
'final_assets_count': len(expanded_assets),
|
||||
'model_statistics': model_stats,
|
||||
'token_usage': {
|
||||
'primary_analysis_total': total_primary_tokens,
|
||||
'consolidation_tokens': consolidation_response.token_usage.get_total(),
|
||||
'grand_total': total_primary_tokens + consolidation_response.token_usage.get_total()
|
||||
},
|
||||
'cost_breakdown': {
|
||||
'primary_analysis_cost': round(total_primary_cost, 4),
|
||||
'consolidation_cost': round(consolidation_cost, 4),
|
||||
'total_cost': round(total_primary_cost + consolidation_cost, 4)
|
||||
},
|
||||
'processing_times': {
|
||||
'consolidation_time': consolidation_response.processing_time,
|
||||
'primary_models_avg_time': sum(r.processing_time for r in analysis_responses if r.success) / max(1, len([r for r in analysis_responses if r.success]))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
"""
|
||||
LLM Service module for Enhanced Brief Processing System
|
||||
Provides abstracted access to multiple LLM providers
|
||||
"""
|
||||
|
||||
from .base_provider import BaseLLMProvider, LLMResponse, TokenUsage
|
||||
from .openai_provider import OpenAIProvider
|
||||
from .google_provider import GoogleProvider
|
||||
from .anthropic_provider import AnthropicProvider
|
||||
from .provider_manager import ProviderManager
|
||||
|
||||
__all__ = [
|
||||
'BaseLLMProvider',
|
||||
'LLMResponse',
|
||||
'TokenUsage',
|
||||
'OpenAIProvider',
|
||||
'GoogleProvider',
|
||||
'AnthropicProvider',
|
||||
'ProviderManager'
|
||||
]
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
"""
|
||||
LLM Service module for Enhanced Brief Processing System
|
||||
Provides abstracted access to multiple LLM providers
|
||||
"""
|
||||
|
||||
from .base_provider import BaseLLMProvider, LLMResponse, TokenUsage
|
||||
from .openai_provider import OpenAIProvider
|
||||
from .google_provider import GoogleProvider
|
||||
from .anthropic_provider import AnthropicProvider
|
||||
from .provider_manager import ProviderManager
|
||||
|
||||
__all__ = [
|
||||
'BaseLLMProvider',
|
||||
'LLMResponse',
|
||||
'TokenUsage',
|
||||
'OpenAIProvider',
|
||||
'GoogleProvider',
|
||||
'AnthropicProvider',
|
||||
'ProviderManager'
|
||||
]
|
||||
|
|
@ -1,375 +0,0 @@
|
|||
"""
|
||||
Anthropic provider implementation for Claude Opus 4.5 and Sonnet 4.5
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
try:
|
||||
from anthropic import AsyncAnthropic
|
||||
anthropic = AsyncAnthropic # Keep reference for compatibility checks
|
||||
except ImportError:
|
||||
AsyncAnthropic = None
|
||||
anthropic = None
|
||||
|
||||
from .base_provider import BaseLLMProvider, LLMResponse, TokenUsage
|
||||
from ..config import config
|
||||
|
||||
class AnthropicProvider(BaseLLMProvider):
|
||||
"""Anthropic Claude provider supporting Opus and Sonnet models"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None, model_name: Optional[str] = None, **kwargs):
|
||||
if AsyncAnthropic is None:
|
||||
raise ImportError("anthropic package not installed. Run: pip install anthropic>=0.67.0")
|
||||
|
||||
provider_config = config.get_provider_config('anthropic')
|
||||
|
||||
super().__init__(
|
||||
api_key=api_key or provider_config['api_key'],
|
||||
model_name=model_name or self._select_model(kwargs.get('model_variant', 'sonnet'), provider_config),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self.temperature = kwargs.get('temperature', provider_config['temperature'])
|
||||
self.max_tokens = kwargs.get('max_tokens', provider_config['max_tokens'])
|
||||
self.thinking_budget = kwargs.get('thinking_budget', provider_config['thinking_budget'])
|
||||
self.timeout = kwargs.get('timeout', provider_config['timeout'])
|
||||
|
||||
self.client = None
|
||||
self._setup_client()
|
||||
|
||||
def _select_model(self, variant: str, provider_config: Dict[str, Any]) -> str:
|
||||
"""Select appropriate Claude model based on variant"""
|
||||
if variant.lower() in ['opus', 'opus4', 'opus45']:
|
||||
return provider_config['model_opus']
|
||||
elif variant.lower() in ['sonnet', 'sonnet4', 'sonnet45']:
|
||||
return provider_config['model_sonnet']
|
||||
else:
|
||||
# Default to Sonnet for better cost-performance ratio
|
||||
return provider_config['model_sonnet']
|
||||
|
||||
def _setup_client(self):
|
||||
"""Initialize AsyncAnthropic client"""
|
||||
try:
|
||||
self.client = AsyncAnthropic(
|
||||
api_key=self.api_key,
|
||||
timeout=self.timeout
|
||||
)
|
||||
self.logger.info(f"AsyncAnthropic client initialized - Model: {self.model_name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to initialize AsyncAnthropic client: {e}")
|
||||
raise
|
||||
|
||||
async def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Generate response using Anthropic Claude"""
|
||||
start_time = time.time()
|
||||
|
||||
# Determine if we need two-call architecture
|
||||
if self.thinking_budget > 0 and schema:
|
||||
self.logger.info(f"Anthropic Two-Call Request - Model: {self.model_name} (thinking: {self.thinking_budget} budget + schema)")
|
||||
return await self._two_call_with_thinking(messages, schema, start_time, **kwargs)
|
||||
else:
|
||||
self.logger.info(f"Anthropic Single-Call Request - Model: {self.model_name}")
|
||||
return await self._single_call(messages, schema, start_time, **kwargs)
|
||||
|
||||
async def _two_call_with_thinking(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Dict[str, Any],
|
||||
start_time: float,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Execute two-call pattern: thinking analysis + schema formatting"""
|
||||
|
||||
try:
|
||||
# Prepare messages for Anthropic
|
||||
system_message, user_messages = self._prepare_messages(messages)
|
||||
|
||||
# === CALL A: Analysis with Thinking (No Forced Tools) ===
|
||||
self.logger.info(" Call A: Analysis with thinking (no forced tools)")
|
||||
|
||||
# Enhance prompt with schema guidance for Call A
|
||||
enhanced_messages = self._add_schema_guidance_to_messages(user_messages, schema)
|
||||
|
||||
call_a_params = {
|
||||
'model': self.model_name,
|
||||
'messages': enhanced_messages,
|
||||
'max_tokens': self.max_tokens,
|
||||
'temperature': self.temperature,
|
||||
'thinking': {"type": "enabled", "budget_tokens": self.thinking_budget},
|
||||
**kwargs
|
||||
}
|
||||
|
||||
if system_message:
|
||||
call_a_params['system'] = system_message
|
||||
|
||||
# Execute Call A (no tools, no tool_choice)
|
||||
analysis_response = await self.client.messages.create(**call_a_params)
|
||||
|
||||
# Extract analysis text
|
||||
analysis_text = self._extract_text_content(analysis_response.content)
|
||||
if not analysis_text:
|
||||
raise Exception("Call A produced no analysis text")
|
||||
|
||||
self.logger.info(f" Call A completed: {len(analysis_text)} chars analysis")
|
||||
|
||||
# === CALL B: Schema Formatting (No Thinking) ===
|
||||
self.logger.info(" Call B: Schema formatting (no thinking)")
|
||||
|
||||
formatting_prompt = f"Convert the following analysis into the required JSON schema. Call extract_structured_data exactly once with the final result.\n\nAnalysis:\n{analysis_text}"
|
||||
|
||||
call_b_params = {
|
||||
'model': self.model_name,
|
||||
'messages': [{"role": "user", "content": formatting_prompt}],
|
||||
'max_tokens': self.max_tokens,
|
||||
'temperature': self.temperature,
|
||||
'tools': [self._create_tool_from_schema(schema)],
|
||||
'tool_choice': {"type": "tool", "name": "extract_structured_data"},
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Execute Call B (no thinking)
|
||||
format_response = await self.client.messages.create(**call_b_params)
|
||||
|
||||
# Extract structured content from tool use
|
||||
structured_content = self._extract_tool_response(format_response.content)
|
||||
if not structured_content:
|
||||
raise Exception("Call B failed to produce structured output")
|
||||
|
||||
self.logger.info(f" Call B completed: Structured JSON extracted")
|
||||
|
||||
# Combine token usage from both calls
|
||||
combined_token_usage = TokenUsage()
|
||||
if hasattr(analysis_response, 'usage'):
|
||||
usage_dict_a = {
|
||||
'input_tokens': getattr(analysis_response.usage, 'input_tokens', 0),
|
||||
'output_tokens': getattr(analysis_response.usage, 'output_tokens', 0),
|
||||
'cache_read_input_tokens': getattr(analysis_response.usage, 'cache_read_input_tokens', 0)
|
||||
}
|
||||
combined_token_usage.add_usage(usage_dict_a)
|
||||
|
||||
if hasattr(format_response, 'usage'):
|
||||
usage_dict_b = {
|
||||
'input_tokens': getattr(format_response.usage, 'input_tokens', 0),
|
||||
'output_tokens': getattr(format_response.usage, 'output_tokens', 0),
|
||||
'cache_read_input_tokens': getattr(format_response.usage, 'cache_read_input_tokens', 0)
|
||||
}
|
||||
combined_token_usage.add_usage(usage_dict_b)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
return LLMResponse(
|
||||
content=structured_content,
|
||||
raw_response={'call_a': analysis_response, 'call_b': format_response},
|
||||
token_usage=combined_token_usage,
|
||||
model_used=self.model_name,
|
||||
provider="anthropic",
|
||||
success=True,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
self.logger.error(f"Anthropic two-call request failed: {e}")
|
||||
|
||||
return LLMResponse(
|
||||
content="",
|
||||
raw_response=None,
|
||||
token_usage=TokenUsage(),
|
||||
model_used=self.model_name,
|
||||
provider="anthropic",
|
||||
success=False,
|
||||
error=str(e),
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
async def _single_call(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]],
|
||||
start_time: float,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Execute single-call pattern: existing behavior for when thinking=0 or no schema"""
|
||||
|
||||
try:
|
||||
# Prepare messages for Anthropic
|
||||
system_message, user_messages = self._prepare_messages(messages)
|
||||
|
||||
# Configure request parameters (no thinking or minimal thinking)
|
||||
request_params = {
|
||||
'model': self.model_name,
|
||||
'messages': user_messages,
|
||||
'max_tokens': self.max_tokens,
|
||||
'temperature': self.temperature,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Add thinking only if no schema (to avoid conflict)
|
||||
if not schema and self.thinking_budget > 0:
|
||||
request_params['thinking'] = {"type": "enabled", "budget_tokens": self.thinking_budget}
|
||||
|
||||
if system_message:
|
||||
request_params['system'] = system_message
|
||||
|
||||
# Handle structured output using tools if schema provided
|
||||
if schema:
|
||||
request_params['tools'] = [self._create_tool_from_schema(schema)]
|
||||
request_params['tool_choice'] = {"type": "tool", "name": "extract_structured_data"}
|
||||
|
||||
# Generate response using async client
|
||||
response = await self.client.messages.create(**request_params)
|
||||
|
||||
# Extract content
|
||||
if schema and response.content:
|
||||
# Look for tool use in response
|
||||
content = self._extract_tool_response(response.content)
|
||||
else:
|
||||
content = response.content[0].text if response.content else ""
|
||||
|
||||
# Extract token usage
|
||||
token_usage = TokenUsage()
|
||||
if hasattr(response, 'usage'):
|
||||
usage_dict = {
|
||||
'input_tokens': getattr(response.usage, 'input_tokens', 0),
|
||||
'output_tokens': getattr(response.usage, 'output_tokens', 0),
|
||||
'cached_input_tokens': getattr(response.usage, 'cache_read_input_tokens', 0)
|
||||
}
|
||||
token_usage.add_usage(usage_dict)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
llm_response = LLMResponse(
|
||||
content=content,
|
||||
raw_response=response,
|
||||
token_usage=token_usage,
|
||||
model_used=self.model_name,
|
||||
provider="anthropic",
|
||||
success=True,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
self.log_response(llm_response)
|
||||
return llm_response
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
self.logger.error(f"Anthropic single-call request failed: {e}")
|
||||
|
||||
return LLMResponse(
|
||||
content="",
|
||||
raw_response=None,
|
||||
token_usage=TokenUsage(),
|
||||
model_used=self.model_name,
|
||||
provider="anthropic",
|
||||
success=False,
|
||||
error=str(e),
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
def _add_schema_guidance_to_messages(self, user_messages: List[Dict[str, str]], schema: Dict[str, Any]) -> List[Dict[str, str]]:
|
||||
"""Add schema guidance to the last user message for Call A"""
|
||||
enhanced_messages = user_messages.copy()
|
||||
|
||||
# Get schema description
|
||||
schema_description = schema.get('description', 'structured data')
|
||||
|
||||
# Add schema guidance to last message
|
||||
if enhanced_messages:
|
||||
last_message = enhanced_messages[-1]
|
||||
original_content = last_message['content']
|
||||
|
||||
schema_guidance = f"\n\nPlease analyze this document and provide your findings according to this schema structure: {schema_description}. Focus on extracting base deliverables with multiplier arrays as specified in the schema."
|
||||
|
||||
enhanced_messages[-1] = {
|
||||
'role': last_message['role'],
|
||||
'content': original_content + schema_guidance
|
||||
}
|
||||
|
||||
return enhanced_messages
|
||||
|
||||
def _extract_text_content(self, content: List[Any]) -> str:
|
||||
"""Extract text content from Anthropic response, ignoring thinking blocks"""
|
||||
text_content = ""
|
||||
for block in content:
|
||||
if hasattr(block, 'type') and block.type == 'text':
|
||||
text_content += block.text
|
||||
return text_content.strip()
|
||||
|
||||
def _prepare_messages(self, messages: List[Dict[str, str]]) -> tuple:
|
||||
"""Separate system messages from user/assistant messages for Anthropic format"""
|
||||
system_message = None
|
||||
user_messages = []
|
||||
|
||||
for message in messages:
|
||||
if message['role'] == 'system':
|
||||
system_message = message['content']
|
||||
else:
|
||||
user_messages.append({
|
||||
'role': message['role'],
|
||||
'content': message['content']
|
||||
})
|
||||
|
||||
return system_message, user_messages
|
||||
|
||||
def _create_tool_from_schema(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Convert JSON schema to Anthropic tool format for structured output"""
|
||||
# Extract schema definition
|
||||
schema_def = schema.get('schema', schema)
|
||||
|
||||
return {
|
||||
"name": "extract_structured_data",
|
||||
"description": schema.get('description', 'Extract structured data from the document'),
|
||||
"input_schema": schema_def
|
||||
}
|
||||
|
||||
def _extract_tool_response(self, content: List[Any]) -> str:
|
||||
"""Extract structured data from tool use response"""
|
||||
for block in content:
|
||||
if hasattr(block, 'type') and block.type == 'tool_use':
|
||||
return json.dumps(block.input)
|
||||
|
||||
# Fallback to text content
|
||||
text_content = ""
|
||||
for block in content:
|
||||
if hasattr(block, 'type') and block.type == 'text':
|
||||
text_content += block.text
|
||||
|
||||
return text_content
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
"""Validate Anthropic configuration"""
|
||||
if not self.api_key or self.api_key == 'your-anthropic-api-key-here':
|
||||
self.logger.error("Anthropic API key not configured")
|
||||
return False
|
||||
|
||||
if AsyncAnthropic is None:
|
||||
self.logger.error("anthropic package not installed")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def estimate_cost(self, input_tokens: int, output_tokens: int, cached_tokens: int = 0) -> float:
|
||||
"""Estimate cost using Anthropic pricing"""
|
||||
if 'opus' in self.model_name.lower():
|
||||
return config.estimate_cost('anthropic-opus45', input_tokens, output_tokens, cached_tokens)
|
||||
else:
|
||||
return config.estimate_cost('anthropic-sonnet45', input_tokens, output_tokens, cached_tokens)
|
||||
|
||||
def get_max_tokens(self) -> int:
|
||||
"""Get maximum token limit for Claude models"""
|
||||
return 200000 # Claude 3 context window
|
||||
|
||||
def get_model_variant(self) -> str:
|
||||
"""Get the model variant (opus or sonnet)"""
|
||||
if 'opus' in self.model_name.lower():
|
||||
return 'opus'
|
||||
else:
|
||||
return 'sonnet'
|
||||
|
|
@ -1,375 +0,0 @@
|
|||
"""
|
||||
Anthropic provider implementation for Claude Opus 4.1 and Sonnet 4
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
try:
|
||||
from anthropic import AsyncAnthropic
|
||||
anthropic = AsyncAnthropic # Keep reference for compatibility checks
|
||||
except ImportError:
|
||||
AsyncAnthropic = None
|
||||
anthropic = None
|
||||
|
||||
from .base_provider import BaseLLMProvider, LLMResponse, TokenUsage
|
||||
from config import config
|
||||
|
||||
class AnthropicProvider(BaseLLMProvider):
|
||||
"""Anthropic Claude provider supporting Opus and Sonnet models"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None, model_name: Optional[str] = None, **kwargs):
|
||||
if AsyncAnthropic is None:
|
||||
raise ImportError("anthropic package not installed. Run: pip install anthropic>=0.67.0")
|
||||
|
||||
provider_config = config.get_provider_config('anthropic')
|
||||
|
||||
super().__init__(
|
||||
api_key=api_key or provider_config['api_key'],
|
||||
model_name=model_name or self._select_model(kwargs.get('model_variant', 'sonnet'), provider_config),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self.temperature = kwargs.get('temperature', provider_config['temperature'])
|
||||
self.max_tokens = kwargs.get('max_tokens', provider_config['max_tokens'])
|
||||
self.thinking_budget = kwargs.get('thinking_budget', provider_config['thinking_budget'])
|
||||
self.timeout = kwargs.get('timeout', provider_config['timeout'])
|
||||
|
||||
self.client = None
|
||||
self._setup_client()
|
||||
|
||||
def _select_model(self, variant: str, provider_config: Dict[str, Any]) -> str:
|
||||
"""Select appropriate Claude model based on variant"""
|
||||
if variant.lower() in ['opus', 'opus4']:
|
||||
return provider_config['model_opus']
|
||||
elif variant.lower() in ['sonnet', 'sonnet4']:
|
||||
return provider_config['model_sonnet']
|
||||
else:
|
||||
# Default to Sonnet for better cost-performance ratio
|
||||
return provider_config['model_sonnet']
|
||||
|
||||
def _setup_client(self):
|
||||
"""Initialize AsyncAnthropic client"""
|
||||
try:
|
||||
self.client = AsyncAnthropic(
|
||||
api_key=self.api_key,
|
||||
timeout=self.timeout
|
||||
)
|
||||
self.logger.info(f"AsyncAnthropic client initialized - Model: {self.model_name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to initialize AsyncAnthropic client: {e}")
|
||||
raise
|
||||
|
||||
async def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Generate response using Anthropic Claude"""
|
||||
start_time = time.time()
|
||||
|
||||
# Determine if we need two-call architecture
|
||||
if self.thinking_budget > 0 and schema:
|
||||
self.logger.info(f"Anthropic Two-Call Request - Model: {self.model_name} (thinking: {self.thinking_budget} budget + schema)")
|
||||
return await self._two_call_with_thinking(messages, schema, start_time, **kwargs)
|
||||
else:
|
||||
self.logger.info(f"Anthropic Single-Call Request - Model: {self.model_name}")
|
||||
return await self._single_call(messages, schema, start_time, **kwargs)
|
||||
|
||||
async def _two_call_with_thinking(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Dict[str, Any],
|
||||
start_time: float,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Execute two-call pattern: thinking analysis + schema formatting"""
|
||||
|
||||
try:
|
||||
# Prepare messages for Anthropic
|
||||
system_message, user_messages = self._prepare_messages(messages)
|
||||
|
||||
# === CALL A: Analysis with Thinking (No Forced Tools) ===
|
||||
self.logger.info(" Call A: Analysis with thinking (no forced tools)")
|
||||
|
||||
# Enhance prompt with schema guidance for Call A
|
||||
enhanced_messages = self._add_schema_guidance_to_messages(user_messages, schema)
|
||||
|
||||
call_a_params = {
|
||||
'model': self.model_name,
|
||||
'messages': enhanced_messages,
|
||||
'max_tokens': self.max_tokens,
|
||||
'temperature': self.temperature,
|
||||
'thinking': {"type": "enabled", "budget_tokens": self.thinking_budget},
|
||||
**kwargs
|
||||
}
|
||||
|
||||
if system_message:
|
||||
call_a_params['system'] = system_message
|
||||
|
||||
# Execute Call A (no tools, no tool_choice)
|
||||
analysis_response = await self.client.messages.create(**call_a_params)
|
||||
|
||||
# Extract analysis text
|
||||
analysis_text = self._extract_text_content(analysis_response.content)
|
||||
if not analysis_text:
|
||||
raise Exception("Call A produced no analysis text")
|
||||
|
||||
self.logger.info(f" Call A completed: {len(analysis_text)} chars analysis")
|
||||
|
||||
# === CALL B: Schema Formatting (No Thinking) ===
|
||||
self.logger.info(" Call B: Schema formatting (no thinking)")
|
||||
|
||||
formatting_prompt = f"Convert the following analysis into the required JSON schema. Call extract_structured_data exactly once with the final result.\n\nAnalysis:\n{analysis_text}"
|
||||
|
||||
call_b_params = {
|
||||
'model': self.model_name,
|
||||
'messages': [{"role": "user", "content": formatting_prompt}],
|
||||
'max_tokens': self.max_tokens,
|
||||
'temperature': self.temperature,
|
||||
'tools': [self._create_tool_from_schema(schema)],
|
||||
'tool_choice': {"type": "tool", "name": "extract_structured_data"},
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Execute Call B (no thinking)
|
||||
format_response = await self.client.messages.create(**call_b_params)
|
||||
|
||||
# Extract structured content from tool use
|
||||
structured_content = self._extract_tool_response(format_response.content)
|
||||
if not structured_content:
|
||||
raise Exception("Call B failed to produce structured output")
|
||||
|
||||
self.logger.info(f" Call B completed: Structured JSON extracted")
|
||||
|
||||
# Combine token usage from both calls
|
||||
combined_token_usage = TokenUsage()
|
||||
if hasattr(analysis_response, 'usage'):
|
||||
usage_dict_a = {
|
||||
'input_tokens': getattr(analysis_response.usage, 'input_tokens', 0),
|
||||
'output_tokens': getattr(analysis_response.usage, 'output_tokens', 0),
|
||||
'cache_read_input_tokens': getattr(analysis_response.usage, 'cache_read_input_tokens', 0)
|
||||
}
|
||||
combined_token_usage.add_usage(usage_dict_a)
|
||||
|
||||
if hasattr(format_response, 'usage'):
|
||||
usage_dict_b = {
|
||||
'input_tokens': getattr(format_response.usage, 'input_tokens', 0),
|
||||
'output_tokens': getattr(format_response.usage, 'output_tokens', 0),
|
||||
'cache_read_input_tokens': getattr(format_response.usage, 'cache_read_input_tokens', 0)
|
||||
}
|
||||
combined_token_usage.add_usage(usage_dict_b)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
return LLMResponse(
|
||||
content=structured_content,
|
||||
raw_response={'call_a': analysis_response, 'call_b': format_response},
|
||||
token_usage=combined_token_usage,
|
||||
model_used=self.model_name,
|
||||
provider="anthropic",
|
||||
success=True,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
self.logger.error(f"Anthropic two-call request failed: {e}")
|
||||
|
||||
return LLMResponse(
|
||||
content="",
|
||||
raw_response=None,
|
||||
token_usage=TokenUsage(),
|
||||
model_used=self.model_name,
|
||||
provider="anthropic",
|
||||
success=False,
|
||||
error=str(e),
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
async def _single_call(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]],
|
||||
start_time: float,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Execute single-call pattern: existing behavior for when thinking=0 or no schema"""
|
||||
|
||||
try:
|
||||
# Prepare messages for Anthropic
|
||||
system_message, user_messages = self._prepare_messages(messages)
|
||||
|
||||
# Configure request parameters (no thinking or minimal thinking)
|
||||
request_params = {
|
||||
'model': self.model_name,
|
||||
'messages': user_messages,
|
||||
'max_tokens': self.max_tokens,
|
||||
'temperature': self.temperature,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Add thinking only if no schema (to avoid conflict)
|
||||
if not schema and self.thinking_budget > 0:
|
||||
request_params['thinking'] = {"type": "enabled", "budget_tokens": self.thinking_budget}
|
||||
|
||||
if system_message:
|
||||
request_params['system'] = system_message
|
||||
|
||||
# Handle structured output using tools if schema provided
|
||||
if schema:
|
||||
request_params['tools'] = [self._create_tool_from_schema(schema)]
|
||||
request_params['tool_choice'] = {"type": "tool", "name": "extract_structured_data"}
|
||||
|
||||
# Generate response using async client
|
||||
response = await self.client.messages.create(**request_params)
|
||||
|
||||
# Extract content
|
||||
if schema and response.content:
|
||||
# Look for tool use in response
|
||||
content = self._extract_tool_response(response.content)
|
||||
else:
|
||||
content = response.content[0].text if response.content else ""
|
||||
|
||||
# Extract token usage
|
||||
token_usage = TokenUsage()
|
||||
if hasattr(response, 'usage'):
|
||||
usage_dict = {
|
||||
'input_tokens': getattr(response.usage, 'input_tokens', 0),
|
||||
'output_tokens': getattr(response.usage, 'output_tokens', 0),
|
||||
'cached_input_tokens': getattr(response.usage, 'cache_read_input_tokens', 0)
|
||||
}
|
||||
token_usage.add_usage(usage_dict)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
llm_response = LLMResponse(
|
||||
content=content,
|
||||
raw_response=response,
|
||||
token_usage=token_usage,
|
||||
model_used=self.model_name,
|
||||
provider="anthropic",
|
||||
success=True,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
self.log_response(llm_response)
|
||||
return llm_response
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
self.logger.error(f"Anthropic single-call request failed: {e}")
|
||||
|
||||
return LLMResponse(
|
||||
content="",
|
||||
raw_response=None,
|
||||
token_usage=TokenUsage(),
|
||||
model_used=self.model_name,
|
||||
provider="anthropic",
|
||||
success=False,
|
||||
error=str(e),
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
def _add_schema_guidance_to_messages(self, user_messages: List[Dict[str, str]], schema: Dict[str, Any]) -> List[Dict[str, str]]:
|
||||
"""Add schema guidance to the last user message for Call A"""
|
||||
enhanced_messages = user_messages.copy()
|
||||
|
||||
# Get schema description
|
||||
schema_description = schema.get('description', 'structured data')
|
||||
|
||||
# Add schema guidance to last message
|
||||
if enhanced_messages:
|
||||
last_message = enhanced_messages[-1]
|
||||
original_content = last_message['content']
|
||||
|
||||
schema_guidance = f"\n\nPlease analyze this document and provide your findings according to this schema structure: {schema_description}. Focus on extracting base deliverables with multiplier arrays as specified in the schema."
|
||||
|
||||
enhanced_messages[-1] = {
|
||||
'role': last_message['role'],
|
||||
'content': original_content + schema_guidance
|
||||
}
|
||||
|
||||
return enhanced_messages
|
||||
|
||||
def _extract_text_content(self, content: List[Any]) -> str:
|
||||
"""Extract text content from Anthropic response, ignoring thinking blocks"""
|
||||
text_content = ""
|
||||
for block in content:
|
||||
if hasattr(block, 'type') and block.type == 'text':
|
||||
text_content += block.text
|
||||
return text_content.strip()
|
||||
|
||||
def _prepare_messages(self, messages: List[Dict[str, str]]) -> tuple:
|
||||
"""Separate system messages from user/assistant messages for Anthropic format"""
|
||||
system_message = None
|
||||
user_messages = []
|
||||
|
||||
for message in messages:
|
||||
if message['role'] == 'system':
|
||||
system_message = message['content']
|
||||
else:
|
||||
user_messages.append({
|
||||
'role': message['role'],
|
||||
'content': message['content']
|
||||
})
|
||||
|
||||
return system_message, user_messages
|
||||
|
||||
def _create_tool_from_schema(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Convert JSON schema to Anthropic tool format for structured output"""
|
||||
# Extract schema definition
|
||||
schema_def = schema.get('schema', schema)
|
||||
|
||||
return {
|
||||
"name": "extract_structured_data",
|
||||
"description": schema.get('description', 'Extract structured data from the document'),
|
||||
"input_schema": schema_def
|
||||
}
|
||||
|
||||
def _extract_tool_response(self, content: List[Any]) -> str:
|
||||
"""Extract structured data from tool use response"""
|
||||
for block in content:
|
||||
if hasattr(block, 'type') and block.type == 'tool_use':
|
||||
return json.dumps(block.input)
|
||||
|
||||
# Fallback to text content
|
||||
text_content = ""
|
||||
for block in content:
|
||||
if hasattr(block, 'type') and block.type == 'text':
|
||||
text_content += block.text
|
||||
|
||||
return text_content
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
"""Validate Anthropic configuration"""
|
||||
if not self.api_key or self.api_key == 'your-anthropic-api-key-here':
|
||||
self.logger.error("Anthropic API key not configured")
|
||||
return False
|
||||
|
||||
if AsyncAnthropic is None:
|
||||
self.logger.error("anthropic package not installed")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def estimate_cost(self, input_tokens: int, output_tokens: int, cached_tokens: int = 0) -> float:
|
||||
"""Estimate cost using Anthropic pricing"""
|
||||
if 'opus' in self.model_name.lower():
|
||||
return config.estimate_cost('anthropic-opus4', input_tokens, output_tokens, cached_tokens)
|
||||
else:
|
||||
return config.estimate_cost('anthropic-sonnet4', input_tokens, output_tokens, cached_tokens)
|
||||
|
||||
def get_max_tokens(self) -> int:
|
||||
"""Get maximum token limit for Claude models"""
|
||||
return 200000 # Claude 3 context window
|
||||
|
||||
def get_model_variant(self) -> str:
|
||||
"""Get the model variant (opus or sonnet)"""
|
||||
if 'opus' in self.model_name.lower():
|
||||
return 'opus'
|
||||
else:
|
||||
return 'sonnet'
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
"""
|
||||
Base provider class for LLM service abstraction
|
||||
Defines common interface that all providers must implement
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import logging
|
||||
|
||||
class ModelType(Enum):
|
||||
GPT51 = "gpt-5.1"
|
||||
CLAUDE_OPUS = "claude-opus-4-5"
|
||||
CLAUDE_SONNET = "claude-sonnet-4-5"
|
||||
GEMINI_PRO = "gemini-3.1-pro"
|
||||
|
||||
@dataclass
|
||||
class TokenUsage:
|
||||
"""Token usage tracking across different providers"""
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
cached_input_tokens: int = 0
|
||||
|
||||
def add_usage(self, usage_dict: Dict[str, int]):
|
||||
"""Add token usage from provider response"""
|
||||
# Safely handle potential None values
|
||||
input_tokens = usage_dict.get('input_tokens') or usage_dict.get('prompt_tokens') or 0
|
||||
output_tokens = usage_dict.get('output_tokens') or usage_dict.get('completion_tokens') or 0
|
||||
cached_tokens = usage_dict.get('cached_input_tokens') or usage_dict.get('prompt_tokens_cached') or 0
|
||||
|
||||
self.input_tokens += input_tokens
|
||||
self.output_tokens += output_tokens
|
||||
self.cached_input_tokens += cached_tokens
|
||||
|
||||
def get_total(self) -> int:
|
||||
"""Get total token count"""
|
||||
return self.input_tokens + self.output_tokens + self.cached_input_tokens
|
||||
|
||||
@dataclass
|
||||
class LLMResponse:
|
||||
"""Standardized response format across all providers"""
|
||||
content: str
|
||||
raw_response: Any
|
||||
token_usage: TokenUsage
|
||||
model_used: str
|
||||
provider: str
|
||||
success: bool = True
|
||||
error: Optional[str] = None
|
||||
processing_time: float = 0.0
|
||||
|
||||
class BaseLLMProvider(ABC):
|
||||
"""Abstract base class for all LLM providers"""
|
||||
|
||||
def __init__(self, api_key: str, model_name: str, **kwargs):
|
||||
self.api_key = api_key
|
||||
self.model_name = model_name
|
||||
self.config = kwargs
|
||||
self.logger = logging.getLogger(f"{self.__class__.__name__}")
|
||||
|
||||
@abstractmethod
|
||||
async def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""
|
||||
Generate response from the LLM provider
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries with 'role' and 'content'
|
||||
schema: Optional JSON schema for structured output
|
||||
**kwargs: Provider-specific parameters
|
||||
|
||||
Returns:
|
||||
LLMResponse object with standardized format
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def validate_config(self) -> bool:
|
||||
"""Validate provider configuration"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def estimate_cost(self, input_tokens: int, output_tokens: int) -> float:
|
||||
"""Estimate cost for token usage"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_max_tokens(self) -> int:
|
||||
"""Get maximum token limit for this provider/model"""
|
||||
pass
|
||||
|
||||
def get_provider_name(self) -> str:
|
||||
"""Get provider name"""
|
||||
return self.__class__.__name__.replace('Provider', '').lower()
|
||||
|
||||
def prepare_messages(self, system_prompt: str, user_prompt: str) -> List[Dict[str, str]]:
|
||||
"""Prepare messages in standard format"""
|
||||
return [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt}
|
||||
]
|
||||
|
||||
def log_response(self, response: LLMResponse, request_info: str = ""):
|
||||
"""Log response details"""
|
||||
self.logger.info(
|
||||
f"{self.get_provider_name().title()} Response - "
|
||||
f"Model: {response.model_used}, "
|
||||
f"Tokens: {response.token_usage.input_tokens} input / {response.token_usage.output_tokens} output, "
|
||||
f"Time: {response.processing_time:.2f}s, "
|
||||
f"Success: {response.success}"
|
||||
+ (f", Request: {request_info}" if request_info else "")
|
||||
)
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
"""
|
||||
Base provider class for LLM service abstraction
|
||||
Defines common interface that all providers must implement
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import logging
|
||||
|
||||
class ModelType(Enum):
|
||||
GPT5 = "gpt-5"
|
||||
CLAUDE_OPUS = "claude-3-opus"
|
||||
CLAUDE_SONNET = "claude-3-5-sonnet"
|
||||
GEMINI_PRO = "gemini-2.5-pro"
|
||||
|
||||
@dataclass
|
||||
class TokenUsage:
|
||||
"""Token usage tracking across different providers"""
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
cached_input_tokens: int = 0
|
||||
|
||||
def add_usage(self, usage_dict: Dict[str, int]):
|
||||
"""Add token usage from provider response"""
|
||||
# Safely handle potential None values
|
||||
input_tokens = usage_dict.get('input_tokens') or usage_dict.get('prompt_tokens') or 0
|
||||
output_tokens = usage_dict.get('output_tokens') or usage_dict.get('completion_tokens') or 0
|
||||
cached_tokens = usage_dict.get('cached_input_tokens') or usage_dict.get('prompt_tokens_cached') or 0
|
||||
|
||||
self.input_tokens += input_tokens
|
||||
self.output_tokens += output_tokens
|
||||
self.cached_input_tokens += cached_tokens
|
||||
|
||||
def get_total(self) -> int:
|
||||
"""Get total token count"""
|
||||
return self.input_tokens + self.output_tokens + self.cached_input_tokens
|
||||
|
||||
@dataclass
|
||||
class LLMResponse:
|
||||
"""Standardized response format across all providers"""
|
||||
content: str
|
||||
raw_response: Any
|
||||
token_usage: TokenUsage
|
||||
model_used: str
|
||||
provider: str
|
||||
success: bool = True
|
||||
error: Optional[str] = None
|
||||
processing_time: float = 0.0
|
||||
|
||||
class BaseLLMProvider(ABC):
|
||||
"""Abstract base class for all LLM providers"""
|
||||
|
||||
def __init__(self, api_key: str, model_name: str, **kwargs):
|
||||
self.api_key = api_key
|
||||
self.model_name = model_name
|
||||
self.config = kwargs
|
||||
self.logger = logging.getLogger(f"{self.__class__.__name__}")
|
||||
|
||||
@abstractmethod
|
||||
async def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""
|
||||
Generate response from the LLM provider
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries with 'role' and 'content'
|
||||
schema: Optional JSON schema for structured output
|
||||
**kwargs: Provider-specific parameters
|
||||
|
||||
Returns:
|
||||
LLMResponse object with standardized format
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def validate_config(self) -> bool:
|
||||
"""Validate provider configuration"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def estimate_cost(self, input_tokens: int, output_tokens: int) -> float:
|
||||
"""Estimate cost for token usage"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_max_tokens(self) -> int:
|
||||
"""Get maximum token limit for this provider/model"""
|
||||
pass
|
||||
|
||||
def get_provider_name(self) -> str:
|
||||
"""Get provider name"""
|
||||
return self.__class__.__name__.replace('Provider', '').lower()
|
||||
|
||||
def prepare_messages(self, system_prompt: str, user_prompt: str) -> List[Dict[str, str]]:
|
||||
"""Prepare messages in standard format"""
|
||||
return [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt}
|
||||
]
|
||||
|
||||
def log_response(self, response: LLMResponse, request_info: str = ""):
|
||||
"""Log response details"""
|
||||
self.logger.info(
|
||||
f"{self.get_provider_name().title()} Response - "
|
||||
f"Model: {response.model_used}, "
|
||||
f"Tokens: {response.token_usage.input_tokens} input / {response.token_usage.output_tokens} output, "
|
||||
f"Time: {response.processing_time:.2f}s, "
|
||||
f"Success: {response.success}"
|
||||
+ (f", Request: {request_info}" if request_info else "")
|
||||
)
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
"""
|
||||
Google provider implementation for Gemini 2.5 Pro using the new google-genai SDK
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai.types import GenerateContentConfig, ThinkingConfig
|
||||
except ImportError:
|
||||
genai = None
|
||||
GenerateContentConfig = None
|
||||
ThinkingConfig = None
|
||||
|
||||
from .base_provider import BaseLLMProvider, LLMResponse, TokenUsage
|
||||
from ..config import config
|
||||
|
||||
class GoogleProvider(BaseLLMProvider):
|
||||
"""Google Gemini 2.5 Pro provider using new google-genai SDK"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None, model_name: Optional[str] = None, **kwargs):
|
||||
if genai is None:
|
||||
raise ImportError("google-genai package not installed. Run: pip install google-genai")
|
||||
|
||||
provider_config = config.get_provider_config('google')
|
||||
|
||||
super().__init__(
|
||||
api_key=api_key or provider_config['api_key'],
|
||||
model_name=model_name or provider_config['model'],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self.temperature = kwargs.get('temperature', provider_config['temperature'])
|
||||
self.max_output_tokens = kwargs.get('max_output_tokens', provider_config['max_output_tokens'])
|
||||
self.thinking_budget = kwargs.get('thinking_budget', provider_config['thinking_budget'])
|
||||
self.timeout = kwargs.get('timeout', provider_config['timeout'])
|
||||
|
||||
self.client = None
|
||||
self._setup_client()
|
||||
|
||||
def _setup_client(self):
|
||||
"""Initialize Google GenAI client"""
|
||||
try:
|
||||
self.client = genai.Client(api_key=self.api_key)
|
||||
self.logger.info(f"Google GenAI client initialized - Model: {self.model_name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to initialize Google GenAI client: {e}")
|
||||
raise
|
||||
|
||||
async def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Generate response using Google Gemini 2.5 Pro"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
self.logger.info(f"Google Request - Model: {self.model_name} (thinking enabled: {self.thinking_budget} budget)")
|
||||
|
||||
# Convert messages to Google format
|
||||
content = self._prepare_content(messages)
|
||||
|
||||
# Configure generation with thinking capabilities
|
||||
config_dict = {
|
||||
'temperature': self.temperature,
|
||||
'max_output_tokens': self.max_output_tokens,
|
||||
'thinking_config': ThinkingConfig(thinking_budget=self.thinking_budget) if ThinkingConfig else None,
|
||||
}
|
||||
|
||||
# Add JSON schema for structured output if provided
|
||||
if schema:
|
||||
config_dict['response_mime_type'] = 'application/json'
|
||||
converted_schema = self._convert_schema_to_google_format(schema)
|
||||
|
||||
# Google GenAI SDK expects response_schema, not response_json_schema
|
||||
config_dict['response_schema'] = converted_schema
|
||||
self.logger.info("Using structured output with converted schema")
|
||||
|
||||
generation_config = GenerateContentConfig(**config_dict)
|
||||
|
||||
# Generate response using native async API
|
||||
response = await self.client.aio.models.generate_content(
|
||||
model=self.model_name,
|
||||
contents=content,
|
||||
config=generation_config
|
||||
)
|
||||
|
||||
# Extract content
|
||||
if hasattr(response, 'text'):
|
||||
content = response.text
|
||||
elif hasattr(response, 'candidates') and response.candidates:
|
||||
content = response.candidates[0].content.parts[0].text
|
||||
else:
|
||||
content = str(response)
|
||||
|
||||
# Extract token usage
|
||||
token_usage = TokenUsage()
|
||||
if hasattr(response, 'usage_metadata'):
|
||||
# Safely extract token counts with proper defaults
|
||||
input_tokens = getattr(response.usage_metadata, 'prompt_token_count', None) or 0
|
||||
output_tokens = getattr(response.usage_metadata, 'candidates_token_count', None) or 0
|
||||
cached_tokens = getattr(response.usage_metadata, 'cached_content_token_count', None) or 0
|
||||
|
||||
usage_dict = {
|
||||
'input_tokens': input_tokens,
|
||||
'output_tokens': output_tokens,
|
||||
'cached_input_tokens': cached_tokens
|
||||
}
|
||||
|
||||
self.logger.debug(f"Google token usage: {usage_dict}")
|
||||
token_usage.add_usage(usage_dict)
|
||||
else:
|
||||
self.logger.warning("No usage_metadata found in Google response")
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
llm_response = LLMResponse(
|
||||
content=content,
|
||||
raw_response=response,
|
||||
token_usage=token_usage,
|
||||
model_used=self.model_name,
|
||||
provider="google",
|
||||
success=True,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
self.log_response(llm_response)
|
||||
return llm_response
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
self.logger.error(f"Google request failed: {e}")
|
||||
|
||||
return LLMResponse(
|
||||
content="",
|
||||
raw_response=None,
|
||||
token_usage=TokenUsage(),
|
||||
model_used=self.model_name,
|
||||
provider="google",
|
||||
success=False,
|
||||
error=str(e),
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
def _prepare_content(self, messages: List[Dict[str, str]]) -> List[Dict[str, Any]]:
|
||||
"""Convert standard messages to Google GenAI format"""
|
||||
contents = []
|
||||
|
||||
for message in messages:
|
||||
role = message['role']
|
||||
text = message['content']
|
||||
|
||||
# Map roles to Google format
|
||||
if role == 'system':
|
||||
# System messages go into parts directly
|
||||
contents.append({
|
||||
'role': 'user', # Google doesn't have explicit system role
|
||||
'parts': [{'text': f"System: {text}"}]
|
||||
})
|
||||
elif role == 'user':
|
||||
contents.append({
|
||||
'role': 'user',
|
||||
'parts': [{'text': text}]
|
||||
})
|
||||
elif role == 'assistant':
|
||||
contents.append({
|
||||
'role': 'model',
|
||||
'parts': [{'text': text}]
|
||||
})
|
||||
|
||||
return contents
|
||||
|
||||
def _convert_schema_to_google_format(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Convert OpenAI JSON schema to Google GenAI format"""
|
||||
|
||||
def convert_type(openai_type: str) -> str:
|
||||
"""Convert OpenAI type to Google GenAI type"""
|
||||
type_mapping = {
|
||||
'string': 'STRING',
|
||||
'array': 'ARRAY',
|
||||
'object': 'OBJECT',
|
||||
'integer': 'INTEGER',
|
||||
'number': 'NUMBER',
|
||||
'boolean': 'BOOLEAN'
|
||||
}
|
||||
return type_mapping.get(openai_type.lower(), 'STRING')
|
||||
|
||||
def convert_schema_node(node):
|
||||
if isinstance(node, dict):
|
||||
converted = {}
|
||||
for key, value in node.items():
|
||||
if key == 'type':
|
||||
# Convert type to Google format
|
||||
converted['type'] = convert_type(value)
|
||||
elif key == 'oneOf':
|
||||
# Google doesn't support oneOf - use the string type option
|
||||
if isinstance(value, list) and len(value) > 0:
|
||||
string_option = next((item for item in value if item.get('type') == 'string'), value[0])
|
||||
return convert_schema_node(string_option)
|
||||
elif key == 'items':
|
||||
# Convert array items
|
||||
converted['items'] = convert_schema_node(value)
|
||||
elif key == 'properties':
|
||||
# Convert object properties
|
||||
converted['properties'] = {}
|
||||
for prop_name, prop_schema in value.items():
|
||||
converted['properties'][prop_name] = convert_schema_node(prop_schema)
|
||||
elif key == 'required':
|
||||
# Keep required fields as-is
|
||||
converted['required'] = value
|
||||
elif key == 'additionalProperties':
|
||||
# Skip additionalProperties - not supported by Gemini API
|
||||
self.logger.debug(f"Skipping unsupported 'additionalProperties' in Google schema")
|
||||
continue
|
||||
elif key in ['description', 'title']:
|
||||
# Keep description and title
|
||||
converted[key] = value
|
||||
# Skip other OpenAI-specific fields like 'name'
|
||||
return converted
|
||||
elif isinstance(node, list):
|
||||
return [convert_schema_node(item) for item in node]
|
||||
else:
|
||||
return node
|
||||
|
||||
# Extract the actual schema from OpenAI format
|
||||
if 'schema' in schema:
|
||||
google_schema = convert_schema_node(schema['schema'])
|
||||
else:
|
||||
google_schema = convert_schema_node(schema)
|
||||
|
||||
return google_schema
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
"""Validate Google configuration"""
|
||||
if not self.api_key or self.api_key == 'your-google-api-key-here':
|
||||
self.logger.error("Google API key not configured")
|
||||
return False
|
||||
|
||||
if genai is None:
|
||||
self.logger.error("google-genai package not installed")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def estimate_cost(self, input_tokens: int, output_tokens: int, cached_tokens: int = 0) -> float:
|
||||
"""Estimate cost using Google Gemini pricing"""
|
||||
return config.estimate_cost('google-gemini31', input_tokens, output_tokens, cached_tokens)
|
||||
|
||||
def get_max_tokens(self) -> int:
|
||||
"""Get maximum token limit for Gemini 3.1 Pro"""
|
||||
return 2000000 # Gemini 3.1 Pro context window
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
"""
|
||||
Google provider implementation for Gemini 2.5 Pro using the new google-genai SDK
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai.types import GenerateContentConfig, ThinkingConfig
|
||||
except ImportError:
|
||||
genai = None
|
||||
GenerateContentConfig = None
|
||||
ThinkingConfig = None
|
||||
|
||||
from .base_provider import BaseLLMProvider, LLMResponse, TokenUsage
|
||||
from config import config
|
||||
|
||||
class GoogleProvider(BaseLLMProvider):
|
||||
"""Google Gemini 2.5 Pro provider using new google-genai SDK"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None, model_name: Optional[str] = None, **kwargs):
|
||||
if genai is None:
|
||||
raise ImportError("google-genai package not installed. Run: pip install google-genai")
|
||||
|
||||
provider_config = config.get_provider_config('google')
|
||||
|
||||
super().__init__(
|
||||
api_key=api_key or provider_config['api_key'],
|
||||
model_name=model_name or provider_config['model'],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self.temperature = kwargs.get('temperature', provider_config['temperature'])
|
||||
self.max_output_tokens = kwargs.get('max_output_tokens', provider_config['max_output_tokens'])
|
||||
self.thinking_budget = kwargs.get('thinking_budget', provider_config['thinking_budget'])
|
||||
self.timeout = kwargs.get('timeout', provider_config['timeout'])
|
||||
|
||||
self.client = None
|
||||
self._setup_client()
|
||||
|
||||
def _setup_client(self):
|
||||
"""Initialize Google GenAI client"""
|
||||
try:
|
||||
self.client = genai.Client(api_key=self.api_key)
|
||||
self.logger.info(f"Google GenAI client initialized - Model: {self.model_name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to initialize Google GenAI client: {e}")
|
||||
raise
|
||||
|
||||
async def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Generate response using Google Gemini 2.5 Pro"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
self.logger.info(f"Google Request - Model: {self.model_name} (thinking enabled: {self.thinking_budget} budget)")
|
||||
|
||||
# Convert messages to Google format
|
||||
content = self._prepare_content(messages)
|
||||
|
||||
# Configure generation with thinking capabilities
|
||||
config_dict = {
|
||||
'temperature': self.temperature,
|
||||
'max_output_tokens': self.max_output_tokens,
|
||||
'thinking_config': ThinkingConfig(thinking_budget=self.thinking_budget) if ThinkingConfig else None,
|
||||
}
|
||||
|
||||
# Add JSON schema for structured output if provided
|
||||
if schema:
|
||||
config_dict['response_mime_type'] = 'application/json'
|
||||
converted_schema = self._convert_schema_to_google_format(schema)
|
||||
|
||||
# Google GenAI SDK expects response_schema, not response_json_schema
|
||||
config_dict['response_schema'] = converted_schema
|
||||
self.logger.info("Using structured output with converted schema")
|
||||
|
||||
generation_config = GenerateContentConfig(**config_dict)
|
||||
|
||||
# Generate response using native async API
|
||||
response = await self.client.aio.models.generate_content(
|
||||
model=self.model_name,
|
||||
contents=content,
|
||||
config=generation_config
|
||||
)
|
||||
|
||||
# Extract content
|
||||
if hasattr(response, 'text'):
|
||||
content = response.text
|
||||
elif hasattr(response, 'candidates') and response.candidates:
|
||||
content = response.candidates[0].content.parts[0].text
|
||||
else:
|
||||
content = str(response)
|
||||
|
||||
# Extract token usage
|
||||
token_usage = TokenUsage()
|
||||
if hasattr(response, 'usage_metadata'):
|
||||
# Safely extract token counts with proper defaults
|
||||
input_tokens = getattr(response.usage_metadata, 'prompt_token_count', None) or 0
|
||||
output_tokens = getattr(response.usage_metadata, 'candidates_token_count', None) or 0
|
||||
cached_tokens = getattr(response.usage_metadata, 'cached_content_token_count', None) or 0
|
||||
|
||||
usage_dict = {
|
||||
'input_tokens': input_tokens,
|
||||
'output_tokens': output_tokens,
|
||||
'cached_input_tokens': cached_tokens
|
||||
}
|
||||
|
||||
self.logger.debug(f"Google token usage: {usage_dict}")
|
||||
token_usage.add_usage(usage_dict)
|
||||
else:
|
||||
self.logger.warning("No usage_metadata found in Google response")
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
llm_response = LLMResponse(
|
||||
content=content,
|
||||
raw_response=response,
|
||||
token_usage=token_usage,
|
||||
model_used=self.model_name,
|
||||
provider="google",
|
||||
success=True,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
self.log_response(llm_response)
|
||||
return llm_response
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
self.logger.error(f"Google request failed: {e}")
|
||||
|
||||
return LLMResponse(
|
||||
content="",
|
||||
raw_response=None,
|
||||
token_usage=TokenUsage(),
|
||||
model_used=self.model_name,
|
||||
provider="google",
|
||||
success=False,
|
||||
error=str(e),
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
def _prepare_content(self, messages: List[Dict[str, str]]) -> List[Dict[str, Any]]:
|
||||
"""Convert standard messages to Google GenAI format"""
|
||||
contents = []
|
||||
|
||||
for message in messages:
|
||||
role = message['role']
|
||||
text = message['content']
|
||||
|
||||
# Map roles to Google format
|
||||
if role == 'system':
|
||||
# System messages go into parts directly
|
||||
contents.append({
|
||||
'role': 'user', # Google doesn't have explicit system role
|
||||
'parts': [{'text': f"System: {text}"}]
|
||||
})
|
||||
elif role == 'user':
|
||||
contents.append({
|
||||
'role': 'user',
|
||||
'parts': [{'text': text}]
|
||||
})
|
||||
elif role == 'assistant':
|
||||
contents.append({
|
||||
'role': 'model',
|
||||
'parts': [{'text': text}]
|
||||
})
|
||||
|
||||
return contents
|
||||
|
||||
def _convert_schema_to_google_format(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Convert OpenAI JSON schema to Google GenAI format"""
|
||||
|
||||
def convert_type(openai_type: str) -> str:
|
||||
"""Convert OpenAI type to Google GenAI type"""
|
||||
type_mapping = {
|
||||
'string': 'STRING',
|
||||
'array': 'ARRAY',
|
||||
'object': 'OBJECT',
|
||||
'integer': 'INTEGER',
|
||||
'number': 'NUMBER',
|
||||
'boolean': 'BOOLEAN'
|
||||
}
|
||||
return type_mapping.get(openai_type.lower(), 'STRING')
|
||||
|
||||
def convert_schema_node(node):
|
||||
if isinstance(node, dict):
|
||||
converted = {}
|
||||
for key, value in node.items():
|
||||
if key == 'type':
|
||||
# Convert type to Google format
|
||||
converted['type'] = convert_type(value)
|
||||
elif key == 'oneOf':
|
||||
# Google doesn't support oneOf - use the string type option
|
||||
if isinstance(value, list) and len(value) > 0:
|
||||
string_option = next((item for item in value if item.get('type') == 'string'), value[0])
|
||||
return convert_schema_node(string_option)
|
||||
elif key == 'items':
|
||||
# Convert array items
|
||||
converted['items'] = convert_schema_node(value)
|
||||
elif key == 'properties':
|
||||
# Convert object properties
|
||||
converted['properties'] = {}
|
||||
for prop_name, prop_schema in value.items():
|
||||
converted['properties'][prop_name] = convert_schema_node(prop_schema)
|
||||
elif key == 'required':
|
||||
# Keep required fields as-is
|
||||
converted['required'] = value
|
||||
elif key == 'additionalProperties':
|
||||
# Skip additionalProperties - not supported by Gemini API
|
||||
self.logger.debug(f"Skipping unsupported 'additionalProperties' in Google schema")
|
||||
continue
|
||||
elif key in ['description', 'title']:
|
||||
# Keep description and title
|
||||
converted[key] = value
|
||||
# Skip other OpenAI-specific fields like 'name'
|
||||
return converted
|
||||
elif isinstance(node, list):
|
||||
return [convert_schema_node(item) for item in node]
|
||||
else:
|
||||
return node
|
||||
|
||||
# Extract the actual schema from OpenAI format
|
||||
if 'schema' in schema:
|
||||
google_schema = convert_schema_node(schema['schema'])
|
||||
else:
|
||||
google_schema = convert_schema_node(schema)
|
||||
|
||||
return google_schema
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
"""Validate Google configuration"""
|
||||
if not self.api_key or self.api_key == 'your-google-api-key-here':
|
||||
self.logger.error("Google API key not configured")
|
||||
return False
|
||||
|
||||
if genai is None:
|
||||
self.logger.error("google-genai package not installed")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def estimate_cost(self, input_tokens: int, output_tokens: int, cached_tokens: int = 0) -> float:
|
||||
"""Estimate cost using Google Gemini pricing"""
|
||||
return config.estimate_cost('google-gemini25', input_tokens, output_tokens, cached_tokens)
|
||||
|
||||
def get_max_tokens(self) -> int:
|
||||
"""Get maximum token limit for Gemini 2.5 Pro"""
|
||||
return 2000000 # Gemini 2.5 Pro context window
|
||||
|
|
@ -1,309 +0,0 @@
|
|||
"""
|
||||
OpenAI provider implementation for GPT-5 with reasoning effort support
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .base_provider import BaseLLMProvider, LLMResponse, TokenUsage
|
||||
from ..config import config
|
||||
|
||||
class OpenAIProvider(BaseLLMProvider):
|
||||
"""OpenAI GPT-5 provider with reasoning effort support"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None, model_name: Optional[str] = None, **kwargs):
|
||||
provider_config = config.get_provider_config('openai')
|
||||
|
||||
super().__init__(
|
||||
api_key=api_key or provider_config['api_key'],
|
||||
model_name=model_name or provider_config['model'],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self.reasoning_effort = kwargs.get('reasoning_effort', provider_config['reasoning_effort'])
|
||||
self.timeout = kwargs.get('timeout', provider_config['timeout'])
|
||||
self.max_retries = kwargs.get('max_retries', provider_config['max_retries'])
|
||||
|
||||
self.client = None
|
||||
self._setup_client()
|
||||
|
||||
def _setup_client(self):
|
||||
"""Initialize AsyncOpenAI client with configuration"""
|
||||
try:
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=self.api_key,
|
||||
timeout=self.timeout,
|
||||
max_retries=self.max_retries
|
||||
)
|
||||
self.logger.info(f"AsyncOpenAI client initialized - Model: {self.model_name}, Reasoning: {self.reasoning_effort}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to initialize AsyncOpenAI client: {e}")
|
||||
raise
|
||||
|
||||
async def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Generate response using OpenAI GPT-5 with reasoning effort"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
self.logger.info(f"OpenAI Request - Model: {self.model_name}, Reasoning: {self.reasoning_effort}")
|
||||
|
||||
if schema:
|
||||
# Use structured output with Pydantic model
|
||||
stage_tag = "[CONSOLIDATION]" if "MODELS' ANALYSIS RESULTS" in str(messages) else "[INITIAL]"
|
||||
self.logger.info(f"{stage_tag} Using structured output with schema: {schema.get('name', 'unknown')}")
|
||||
schema_model = self._create_pydantic_model(schema)
|
||||
self.logger.debug(f"{stage_tag} Created Pydantic model: {schema_model.__name__}")
|
||||
|
||||
response = await self.client.responses.parse(
|
||||
model=self.model_name,
|
||||
input=messages,
|
||||
reasoning={"effort": self.reasoning_effort},
|
||||
text_format=schema_model
|
||||
)
|
||||
|
||||
# Extract structured content
|
||||
if hasattr(response, 'output_parsed') and response.output_parsed is not None:
|
||||
try:
|
||||
# Extract JSON from Pydantic model
|
||||
content = response.output_parsed.model_dump_json()
|
||||
|
||||
# Validate the content has expected structure
|
||||
try:
|
||||
parsed_content = json.loads(content)
|
||||
|
||||
if not isinstance(parsed_content, dict):
|
||||
self.logger.error(f"{stage_tag} Structured output is not a dict: {type(parsed_content)}")
|
||||
raise ValueError("Expected dict structure")
|
||||
|
||||
if 'assets' not in parsed_content:
|
||||
# PROBLEM DETECTED - Log everything verbosely
|
||||
self.logger.error(f"{stage_tag} ========== MISSING 'assets' KEY - VERBOSE DEBUG ==========")
|
||||
self.logger.error(f"{stage_tag} Response type: {type(response).__name__}")
|
||||
self.logger.error(f"{stage_tag} Has output_parsed: {hasattr(response, 'output_parsed')}")
|
||||
self.logger.error(f"{stage_tag} output_parsed type: {type(response.output_parsed)}")
|
||||
self.logger.error(f"{stage_tag} Raw output_parsed value: {response.output_parsed}")
|
||||
self.logger.error(f"{stage_tag} Extracted JSON length: {len(content)} chars")
|
||||
self.logger.error(f"{stage_tag} Full JSON content: {content}")
|
||||
self.logger.error(f"{stage_tag} Parsed data keys: {list(parsed_content.keys())}")
|
||||
self.logger.error(f"{stage_tag} Full parsed content: {parsed_content}")
|
||||
|
||||
# Try to fix common issues
|
||||
if not parsed_content: # Empty dict
|
||||
self.logger.warning(f"{stage_tag} Got empty dict, creating default structure")
|
||||
content = json.dumps({"assets": []})
|
||||
self.logger.info(f"{stage_tag} Fixed content: {content}")
|
||||
else:
|
||||
# Save to file and fail
|
||||
self._save_debug_response(response, content, stage_tag)
|
||||
raise KeyError("Missing assets key")
|
||||
else:
|
||||
# SUCCESS - Just log summary
|
||||
assets_count = len(parsed_content.get('assets', []))
|
||||
self.logger.info(f"{stage_tag} Structured output validated: {assets_count} assets")
|
||||
|
||||
except json.JSONDecodeError as je:
|
||||
self.logger.error(f"Failed to parse structured output as JSON: {je}")
|
||||
self.logger.error(f"Raw structured content: {content[:500]}...")
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing structured output: {e}")
|
||||
self.logger.error(f"Raw response object: {str(response)[:500]}...")
|
||||
raise
|
||||
else:
|
||||
self.logger.error(f"{stage_tag} No structured output found in response (output_parsed is None)")
|
||||
self.logger.error(f"{stage_tag} Response attributes: {dir(response)}")
|
||||
|
||||
# Save debug info
|
||||
self._save_debug_response(response, None, stage_tag)
|
||||
|
||||
# Fallback to raw response content if available
|
||||
if hasattr(response, 'choices') and response.choices:
|
||||
fallback_content = response.choices[0].message.content
|
||||
self.logger.warning(f"{stage_tag} Using fallback content from choices: {len(fallback_content) if fallback_content else 0} chars")
|
||||
# Try to parse the fallback content as JSON
|
||||
if fallback_content:
|
||||
try:
|
||||
parsed = json.loads(fallback_content)
|
||||
content = fallback_content
|
||||
self.logger.info(f"{stage_tag} Successfully parsed fallback content as JSON")
|
||||
except json.JSONDecodeError:
|
||||
self.logger.error(f"{stage_tag} Fallback content is not valid JSON: {fallback_content[:500]}")
|
||||
content = json.dumps({"assets": []}) # Empty default
|
||||
else:
|
||||
self.logger.warning(f"{stage_tag} No fallback content, using empty assets array")
|
||||
content = json.dumps({"assets": []}) # Empty default
|
||||
else:
|
||||
self.logger.error(f"{stage_tag} No fallback content available in response")
|
||||
self.logger.error(f"{stage_tag} Response has choices: {hasattr(response, 'choices')}")
|
||||
content = json.dumps({"assets": []}) # Empty default structure
|
||||
|
||||
else:
|
||||
# Use regular chat completion
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
**kwargs
|
||||
)
|
||||
content = response.choices[0].message.content
|
||||
|
||||
# Extract token usage
|
||||
token_usage = TokenUsage()
|
||||
if hasattr(response, 'usage'):
|
||||
usage_dict = {
|
||||
'input_tokens': getattr(response.usage, 'input_tokens', getattr(response.usage, 'prompt_tokens', 0)),
|
||||
'output_tokens': getattr(response.usage, 'output_tokens', getattr(response.usage, 'completion_tokens', 0)),
|
||||
'cached_input_tokens': getattr(response.usage, 'input_tokens_cached', getattr(response.usage, 'prompt_tokens_cached', 0))
|
||||
}
|
||||
token_usage.add_usage(usage_dict)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
llm_response = LLMResponse(
|
||||
content=content,
|
||||
raw_response=response,
|
||||
token_usage=token_usage,
|
||||
model_used=self.model_name,
|
||||
provider="openai",
|
||||
success=True,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
self.log_response(llm_response, f"Reasoning: {self.reasoning_effort}")
|
||||
return llm_response
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
self.logger.error(f"OpenAI request failed: {e}")
|
||||
|
||||
return LLMResponse(
|
||||
content="",
|
||||
raw_response=None,
|
||||
token_usage=TokenUsage(),
|
||||
model_used=self.model_name,
|
||||
provider="openai",
|
||||
success=False,
|
||||
error=str(e),
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
def _create_pydantic_model(self, schema: Dict[str, Any]) -> BaseModel:
|
||||
"""Create Pydantic model from JSON schema for structured output"""
|
||||
try:
|
||||
# For base deliverable extraction, we can use the existing models
|
||||
from ..process_brief_enhanced import BaseExtractionResult
|
||||
return BaseExtractionResult
|
||||
except ImportError as e:
|
||||
self.logger.warning(f"Failed to import BaseExtractionResult: {e}, using dynamic model")
|
||||
# Fallback: create dynamic model with proper nested structure
|
||||
from pydantic import create_model
|
||||
|
||||
# Handle nested schema structure properly
|
||||
try:
|
||||
# Create dynamic models for nested structures
|
||||
schema_props = schema.get('schema', {}).get('properties', {})
|
||||
|
||||
# Handle the assets array specifically
|
||||
if 'assets' in schema_props:
|
||||
assets_def = schema_props['assets']
|
||||
if assets_def.get('type') == 'array':
|
||||
item_def = assets_def.get('items', {})
|
||||
item_props = item_def.get('properties', {})
|
||||
|
||||
# Create fields for the asset item model
|
||||
asset_fields = {}
|
||||
for field_name, field_def in item_props.items():
|
||||
if field_def.get('type') == 'array':
|
||||
asset_fields[field_name] = (Optional[List[str]], [])
|
||||
else:
|
||||
asset_fields[field_name] = (Optional[str], "")
|
||||
|
||||
# Create the asset item model
|
||||
AssetModel = create_model('DynamicAssetModel', **asset_fields)
|
||||
|
||||
# Create the main response model with assets array
|
||||
return create_model('DynamicResponseModel', assets=(List[AssetModel], ...))
|
||||
|
||||
# Fallback to simple structure
|
||||
fields = {'assets': (List[Any], ...)}
|
||||
return create_model('DynamicModel', **fields)
|
||||
|
||||
except Exception as schema_error:
|
||||
self.logger.error(f"Failed to create dynamic model from schema: {schema_error}")
|
||||
# Ultimate fallback
|
||||
return create_model('FallbackModel', assets=(List[Any], ...))
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
"""Validate OpenAI configuration"""
|
||||
if not self.api_key or self.api_key == 'your-openai-api-key-here':
|
||||
self.logger.error("OpenAI API key not configured")
|
||||
return False
|
||||
|
||||
if self.reasoning_effort not in ['high', 'medium', 'low', 'minimal']:
|
||||
self.logger.warning(f"Invalid reasoning effort: {self.reasoning_effort}, using 'medium'")
|
||||
self.reasoning_effort = 'medium'
|
||||
|
||||
return True
|
||||
|
||||
def estimate_cost(self, input_tokens: int, output_tokens: int, cached_tokens: int = 0) -> float:
|
||||
"""Estimate cost using OpenAI GPT-5.1 pricing"""
|
||||
return config.estimate_cost('openai-gpt51', input_tokens, output_tokens, cached_tokens)
|
||||
|
||||
def get_max_tokens(self) -> int:
|
||||
"""Get maximum token limit for GPT-5.1"""
|
||||
return 200000 # GPT-5.1 context window
|
||||
|
||||
def set_reasoning_effort(self, effort: str):
|
||||
"""Update reasoning effort setting"""
|
||||
if effort in ['high', 'medium', 'low', 'minimal']:
|
||||
self.reasoning_effort = effort
|
||||
self.logger.info(f"Updated reasoning effort to: {effort}")
|
||||
else:
|
||||
self.logger.warning(f"Invalid reasoning effort: {effort}, keeping current: {self.reasoning_effort}")
|
||||
|
||||
def _save_debug_response(self, response, content, stage_tag):
|
||||
"""Save debug information about problematic responses"""
|
||||
try:
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
debug_file = os.path.join(tempfile.gettempdir(), f"openai_debug_{stage_tag.strip('[]')}_{timestamp}.txt")
|
||||
|
||||
with open(debug_file, 'w') as f:
|
||||
f.write(f"=== OpenAI Response Debug {stage_tag} ===\n")
|
||||
f.write(f"Timestamp: {timestamp}\n")
|
||||
f.write(f"Model: {self.model_name}\n")
|
||||
f.write(f"Reasoning: {self.reasoning_effort}\n\n")
|
||||
|
||||
f.write("=== Response Object ===\n")
|
||||
f.write(f"Type: {type(response)}\n")
|
||||
f.write(f"Dir: {dir(response)}\n\n")
|
||||
|
||||
if hasattr(response, 'output_parsed'):
|
||||
f.write(f"output_parsed: {response.output_parsed}\n")
|
||||
f.write(f"output_parsed type: {type(response.output_parsed)}\n\n")
|
||||
|
||||
if hasattr(response, 'choices'):
|
||||
f.write(f"Has choices: {len(response.choices) if response.choices else 0}\n")
|
||||
if response.choices:
|
||||
f.write(f"choices[0]: {response.choices[0]}\n\n")
|
||||
|
||||
f.write("=== Extracted Content ===\n")
|
||||
f.write(f"Content: {content}\n\n")
|
||||
|
||||
f.write("=== Full Response ===\n")
|
||||
f.write(f"{response}\n")
|
||||
|
||||
self.logger.error(f"{stage_tag} Debug info saved to: {debug_file}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"{stage_tag} Failed to save debug info: {e}")
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
"""
|
||||
OpenAI provider implementation for GPT-5 with reasoning effort support
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .base_provider import BaseLLMProvider, LLMResponse, TokenUsage
|
||||
from config import config
|
||||
|
||||
class OpenAIProvider(BaseLLMProvider):
|
||||
"""OpenAI GPT-5 provider with reasoning effort support"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None, model_name: Optional[str] = None, **kwargs):
|
||||
provider_config = config.get_provider_config('openai')
|
||||
|
||||
super().__init__(
|
||||
api_key=api_key or provider_config['api_key'],
|
||||
model_name=model_name or provider_config['model'],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self.reasoning_effort = kwargs.get('reasoning_effort', provider_config['reasoning_effort'])
|
||||
self.timeout = kwargs.get('timeout', provider_config['timeout'])
|
||||
self.max_retries = kwargs.get('max_retries', provider_config['max_retries'])
|
||||
|
||||
self.client = None
|
||||
self._setup_client()
|
||||
|
||||
def _setup_client(self):
|
||||
"""Initialize AsyncOpenAI client with configuration"""
|
||||
try:
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=self.api_key,
|
||||
timeout=self.timeout,
|
||||
max_retries=self.max_retries
|
||||
)
|
||||
self.logger.info(f"AsyncOpenAI client initialized - Model: {self.model_name}, Reasoning: {self.reasoning_effort}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to initialize AsyncOpenAI client: {e}")
|
||||
raise
|
||||
|
||||
async def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> LLMResponse:
|
||||
"""Generate response using OpenAI GPT-5 with reasoning effort"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
self.logger.info(f"OpenAI Request - Model: {self.model_name}, Reasoning: {self.reasoning_effort}")
|
||||
|
||||
if schema:
|
||||
# Use structured output with Pydantic model
|
||||
schema_model = self._create_pydantic_model(schema)
|
||||
|
||||
response = await self.client.responses.parse(
|
||||
model=self.model_name,
|
||||
input=messages,
|
||||
reasoning={"effort": self.reasoning_effort},
|
||||
text_format=schema_model
|
||||
)
|
||||
|
||||
# Extract structured content
|
||||
content = response.output_parsed.model_dump_json() if hasattr(response, 'output_parsed') else str(response)
|
||||
|
||||
else:
|
||||
# Use regular chat completion
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
**kwargs
|
||||
)
|
||||
content = response.choices[0].message.content
|
||||
|
||||
# Extract token usage
|
||||
token_usage = TokenUsage()
|
||||
if hasattr(response, 'usage'):
|
||||
usage_dict = {
|
||||
'input_tokens': getattr(response.usage, 'input_tokens', getattr(response.usage, 'prompt_tokens', 0)),
|
||||
'output_tokens': getattr(response.usage, 'output_tokens', getattr(response.usage, 'completion_tokens', 0)),
|
||||
'cached_input_tokens': getattr(response.usage, 'input_tokens_cached', getattr(response.usage, 'prompt_tokens_cached', 0))
|
||||
}
|
||||
token_usage.add_usage(usage_dict)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
llm_response = LLMResponse(
|
||||
content=content,
|
||||
raw_response=response,
|
||||
token_usage=token_usage,
|
||||
model_used=self.model_name,
|
||||
provider="openai",
|
||||
success=True,
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
self.log_response(llm_response, f"Reasoning: {self.reasoning_effort}")
|
||||
return llm_response
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
self.logger.error(f"OpenAI request failed: {e}")
|
||||
|
||||
return LLMResponse(
|
||||
content="",
|
||||
raw_response=None,
|
||||
token_usage=TokenUsage(),
|
||||
model_used=self.model_name,
|
||||
provider="openai",
|
||||
success=False,
|
||||
error=str(e),
|
||||
processing_time=processing_time
|
||||
)
|
||||
|
||||
def _create_pydantic_model(self, schema: Dict[str, Any]) -> BaseModel:
|
||||
"""Create Pydantic model from JSON schema for structured output"""
|
||||
try:
|
||||
# For base deliverable extraction, we can use the existing models
|
||||
from process_brief_enhanced import BaseExtractionResult
|
||||
return BaseExtractionResult
|
||||
except ImportError:
|
||||
# Fallback: create dynamic model (simplified)
|
||||
from pydantic import create_model
|
||||
|
||||
# This is a simplified version - in practice you'd want more sophisticated schema conversion
|
||||
fields = {}
|
||||
for field_name, field_def in schema.get('properties', {}).items():
|
||||
if field_def.get('type') == 'array':
|
||||
fields[field_name] = (List[Any], ...)
|
||||
else:
|
||||
fields[field_name] = (str, ...)
|
||||
|
||||
return create_model('DynamicModel', **fields)
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
"""Validate OpenAI configuration"""
|
||||
if not self.api_key or self.api_key == 'your-openai-api-key-here':
|
||||
self.logger.error("OpenAI API key not configured")
|
||||
return False
|
||||
|
||||
if self.reasoning_effort not in ['high', 'medium', 'low', 'minimal']:
|
||||
self.logger.warning(f"Invalid reasoning effort: {self.reasoning_effort}, using 'medium'")
|
||||
self.reasoning_effort = 'medium'
|
||||
|
||||
return True
|
||||
|
||||
def estimate_cost(self, input_tokens: int, output_tokens: int, cached_tokens: int = 0) -> float:
|
||||
"""Estimate cost using OpenAI GPT-5 pricing"""
|
||||
return config.estimate_cost('openai-gpt5', input_tokens, output_tokens, cached_tokens)
|
||||
|
||||
def get_max_tokens(self) -> int:
|
||||
"""Get maximum token limit for GPT-5"""
|
||||
return 200000 # GPT-5 context window
|
||||
|
||||
def set_reasoning_effort(self, effort: str):
|
||||
"""Update reasoning effort setting"""
|
||||
if effort in ['high', 'medium', 'low', 'minimal']:
|
||||
self.reasoning_effort = effort
|
||||
self.logger.info(f"Updated reasoning effort to: {effort}")
|
||||
else:
|
||||
self.logger.warning(f"Invalid reasoning effort: {effort}, keeping current: {self.reasoning_effort}")
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
"""
|
||||
Provider manager for coordinating parallel execution across multiple LLM providers
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
import time
|
||||
|
||||
from .base_provider import BaseLLMProvider, LLMResponse, TokenUsage
|
||||
from .openai_provider import OpenAIProvider
|
||||
from .google_provider import GoogleProvider
|
||||
from .anthropic_provider import AnthropicProvider
|
||||
from ..config import config
|
||||
|
||||
class ProviderManager:
|
||||
"""Manages multiple LLM providers and coordinates parallel execution"""
|
||||
|
||||
def __init__(self):
|
||||
self.providers: Dict[str, BaseLLMProvider] = {}
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
def create_provider(self, model_key: str) -> BaseLLMProvider:
|
||||
"""Create provider instance for given model key"""
|
||||
try:
|
||||
provider_name, model_name = config.get_model_info(model_key)
|
||||
|
||||
if provider_name == 'openai':
|
||||
return OpenAIProvider(model_name=model_name)
|
||||
elif provider_name == 'google':
|
||||
return GoogleProvider(model_name=model_name)
|
||||
elif provider_name == 'anthropic':
|
||||
# Extract variant from model key for Anthropic
|
||||
variant = 'opus' if 'opus' in model_key else 'sonnet'
|
||||
return AnthropicProvider(model_name=model_name, model_variant=variant)
|
||||
else:
|
||||
raise ValueError(f"Unknown provider: {provider_name}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to create provider for {model_key}: {e}")
|
||||
raise
|
||||
|
||||
def get_provider(self, model_key: str) -> BaseLLMProvider:
|
||||
"""Get or create provider for model key"""
|
||||
if model_key not in self.providers:
|
||||
self.providers[model_key] = self.create_provider(model_key)
|
||||
return self.providers[model_key]
|
||||
|
||||
async def execute_parallel_analysis(
|
||||
self,
|
||||
model_keys: List[str],
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
minimum_success_threshold: int = 1,
|
||||
on_model_event: Optional[callable] = None
|
||||
) -> Tuple[List[LLMResponse], Dict[str, Any]]:
|
||||
"""
|
||||
Execute analysis across multiple models in parallel
|
||||
|
||||
Args:
|
||||
model_keys: List of model identifiers to use
|
||||
messages: Messages to send to all models
|
||||
schema: Optional JSON schema for structured output
|
||||
minimum_success_threshold: Minimum number of successful responses required
|
||||
on_model_event: Optional callback for model start/end events
|
||||
|
||||
Returns:
|
||||
Tuple of (successful_responses, metadata)
|
||||
"""
|
||||
self.logger.info(f"Starting parallel analysis with models: {model_keys}")
|
||||
start_time = time.time()
|
||||
|
||||
# Validate model keys
|
||||
valid_model_keys = []
|
||||
for model_key in model_keys:
|
||||
try:
|
||||
provider = self.get_provider(model_key)
|
||||
if provider.validate_config():
|
||||
valid_model_keys.append(model_key)
|
||||
else:
|
||||
self.logger.warning(f"Skipping {model_key} due to configuration issues")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to validate {model_key}: {e}")
|
||||
|
||||
if len(valid_model_keys) == 0:
|
||||
raise ValueError("No valid models available for analysis")
|
||||
|
||||
if len(valid_model_keys) < minimum_success_threshold:
|
||||
self.logger.warning(
|
||||
f"Only {len(valid_model_keys)} valid models, but minimum threshold is {minimum_success_threshold}"
|
||||
)
|
||||
|
||||
# Create tasks for parallel execution
|
||||
tasks = []
|
||||
for model_key in valid_model_keys:
|
||||
provider = self.get_provider(model_key)
|
||||
task = asyncio.create_task(
|
||||
self._execute_with_provider(provider, model_key, messages, schema, on_model_event)
|
||||
)
|
||||
tasks.append((model_key, task))
|
||||
|
||||
# Execute all tasks in parallel using asyncio.gather
|
||||
results = []
|
||||
successful_responses = []
|
||||
failed_responses = []
|
||||
|
||||
# Await all tasks simultaneously
|
||||
task_results = await asyncio.gather(*[task for _, task in tasks], return_exceptions=True)
|
||||
|
||||
# Process results
|
||||
for i, (model_key, task) in enumerate(tasks):
|
||||
result = task_results[i]
|
||||
|
||||
if isinstance(result, Exception):
|
||||
self.logger.error(f"Task for {model_key} raised exception: {result}")
|
||||
failed_responses.append((model_key, str(result)))
|
||||
else:
|
||||
response = result
|
||||
results.append((model_key, response))
|
||||
|
||||
if response.success:
|
||||
successful_responses.append(response)
|
||||
# Try to parse the response to count deliverables
|
||||
deliverable_count = self._count_deliverables_in_response(response.content)
|
||||
self.logger.info(f"{model_key} analysis completed successfully - found {deliverable_count} deliverables")
|
||||
else:
|
||||
failed_responses.append((model_key, response.error))
|
||||
self.logger.warning(f"{model_key} analysis failed: {response.error}")
|
||||
|
||||
total_time = time.time() - start_time
|
||||
|
||||
# Check if we meet minimum success threshold
|
||||
if len(successful_responses) < minimum_success_threshold:
|
||||
raise RuntimeError(
|
||||
f"Only {len(successful_responses)} models succeeded, "
|
||||
f"but minimum threshold is {minimum_success_threshold}"
|
||||
)
|
||||
|
||||
# Compile metadata
|
||||
metadata = {
|
||||
'total_models_requested': len(model_keys),
|
||||
'valid_models': len(valid_model_keys),
|
||||
'successful_models': len(successful_responses),
|
||||
'failed_models': len(failed_responses),
|
||||
'total_processing_time': total_time,
|
||||
'model_results': {
|
||||
model_key: {
|
||||
'success': response.success,
|
||||
'processing_time': response.processing_time,
|
||||
'tokens_used': response.token_usage.get_total(),
|
||||
'provider': response.provider,
|
||||
'model': response.model_used,
|
||||
'error': response.error
|
||||
} for model_key, response in results
|
||||
},
|
||||
'failures': failed_responses
|
||||
}
|
||||
|
||||
self.logger.info(
|
||||
f"Parallel analysis completed - {len(successful_responses)}/{len(valid_model_keys)} "
|
||||
f"models succeeded in {total_time:.2f}s"
|
||||
)
|
||||
|
||||
return successful_responses, metadata
|
||||
|
||||
def _count_deliverables_in_response(self, content: str) -> int:
|
||||
"""Count the number of deliverables in a model's JSON response"""
|
||||
try:
|
||||
import json
|
||||
data = json.loads(content)
|
||||
if isinstance(data, dict) and 'assets' in data:
|
||||
return len(data['assets'])
|
||||
return 0
|
||||
except (json.JSONDecodeError, KeyError, TypeError):
|
||||
return 0
|
||||
|
||||
async def _execute_with_provider(
|
||||
self,
|
||||
provider: BaseLLMProvider,
|
||||
model_key: str,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
on_model_event: Optional[callable] = None
|
||||
) -> LLMResponse:
|
||||
"""Execute analysis with a single provider"""
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
self.logger.debug(f"Starting analysis with {model_key}")
|
||||
|
||||
# Notify start event
|
||||
if on_model_event:
|
||||
await on_model_event(model_key, 'start', {
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
})
|
||||
|
||||
start_time = time.time()
|
||||
response = await provider.generate_response(messages, schema)
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
# Calculate cost if possible
|
||||
cost = 0.0
|
||||
try:
|
||||
cost = provider.estimate_cost(
|
||||
response.token_usage.input_tokens,
|
||||
response.token_usage.output_tokens,
|
||||
response.token_usage.cached_input_tokens
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Notify success event
|
||||
if on_model_event:
|
||||
await on_model_event(model_key, 'end', {
|
||||
'response': response,
|
||||
'cost': cost,
|
||||
'processing_time': processing_time,
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Provider {model_key} execution failed: {e}")
|
||||
|
||||
# Notify error event
|
||||
if on_model_event:
|
||||
await on_model_event(model_key, 'end', {
|
||||
'error': str(e),
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
})
|
||||
|
||||
return LLMResponse(
|
||||
content="",
|
||||
raw_response=None,
|
||||
token_usage=TokenUsage(),
|
||||
model_used=model_key,
|
||||
provider=provider.get_provider_name(),
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
def estimate_total_cost(self, model_keys: List[str], estimated_input_tokens: int, estimated_output_tokens: int) -> Dict[str, float]:
|
||||
"""Estimate total cost for all models"""
|
||||
cost_breakdown = {}
|
||||
total_cost = 0.0
|
||||
|
||||
for model_key in model_keys:
|
||||
try:
|
||||
provider = self.get_provider(model_key)
|
||||
model_cost = provider.estimate_cost(estimated_input_tokens, estimated_output_tokens)
|
||||
cost_breakdown[model_key] = model_cost
|
||||
total_cost += model_cost
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not estimate cost for {model_key}: {e}")
|
||||
cost_breakdown[model_key] = 0.0
|
||||
|
||||
cost_breakdown['total'] = total_cost
|
||||
return cost_breakdown
|
||||
|
||||
def get_aggregated_token_usage(self, responses: List[LLMResponse]) -> TokenUsage:
|
||||
"""Aggregate token usage from multiple responses"""
|
||||
total_usage = TokenUsage()
|
||||
|
||||
for response in responses:
|
||||
total_usage.input_tokens += response.token_usage.input_tokens
|
||||
total_usage.output_tokens += response.token_usage.output_tokens
|
||||
total_usage.cached_input_tokens += response.token_usage.cached_input_tokens
|
||||
|
||||
return total_usage
|
||||
|
||||
def get_actual_cost_breakdown(self, responses: List[LLMResponse]) -> Dict[str, float]:
|
||||
"""Calculate actual costs from responses"""
|
||||
cost_breakdown = {}
|
||||
total_cost = 0.0
|
||||
|
||||
for response in responses:
|
||||
try:
|
||||
provider = self.providers.get(response.model_used)
|
||||
if provider:
|
||||
cost = provider.estimate_cost(
|
||||
response.token_usage.input_tokens,
|
||||
response.token_usage.output_tokens,
|
||||
response.token_usage.cached_input_tokens
|
||||
)
|
||||
cost_breakdown[response.model_used] = cost
|
||||
total_cost += cost
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not calculate cost for {response.model_used}: {e}")
|
||||
|
||||
cost_breakdown['total'] = total_cost
|
||||
return cost_breakdown
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
"""
|
||||
Provider manager for coordinating parallel execution across multiple LLM providers
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
import time
|
||||
|
||||
from .base_provider import BaseLLMProvider, LLMResponse, TokenUsage
|
||||
from .openai_provider import OpenAIProvider
|
||||
from .google_provider import GoogleProvider
|
||||
from .anthropic_provider import AnthropicProvider
|
||||
from ..config import config
|
||||
|
||||
class ProviderManager:
|
||||
"""Manages multiple LLM providers and coordinates parallel execution"""
|
||||
|
||||
def __init__(self):
|
||||
self.providers: Dict[str, BaseLLMProvider] = {}
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
def create_provider(self, model_key: str) -> BaseLLMProvider:
|
||||
"""Create provider instance for given model key"""
|
||||
try:
|
||||
provider_name, model_name = config.get_model_info(model_key)
|
||||
|
||||
if provider_name == 'openai':
|
||||
return OpenAIProvider(model_name=model_name)
|
||||
elif provider_name == 'google':
|
||||
return GoogleProvider(model_name=model_name)
|
||||
elif provider_name == 'anthropic':
|
||||
# Extract variant from model key for Anthropic
|
||||
variant = 'opus' if 'opus' in model_key else 'sonnet'
|
||||
return AnthropicProvider(model_name=model_name, model_variant=variant)
|
||||
else:
|
||||
raise ValueError(f"Unknown provider: {provider_name}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to create provider for {model_key}: {e}")
|
||||
raise
|
||||
|
||||
def get_provider(self, model_key: str) -> BaseLLMProvider:
|
||||
"""Get or create provider for model key"""
|
||||
if model_key not in self.providers:
|
||||
self.providers[model_key] = self.create_provider(model_key)
|
||||
return self.providers[model_key]
|
||||
|
||||
async def execute_parallel_analysis(
|
||||
self,
|
||||
model_keys: List[str],
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
minimum_success_threshold: int = 1,
|
||||
on_model_event: Optional[callable] = None
|
||||
) -> Tuple[List[LLMResponse], Dict[str, Any]]:
|
||||
"""
|
||||
Execute analysis across multiple models in parallel
|
||||
|
||||
Args:
|
||||
model_keys: List of model identifiers to use
|
||||
messages: Messages to send to all models
|
||||
schema: Optional JSON schema for structured output
|
||||
minimum_success_threshold: Minimum number of successful responses required
|
||||
on_model_event: Optional callback for model start/end events
|
||||
|
||||
Returns:
|
||||
Tuple of (successful_responses, metadata)
|
||||
"""
|
||||
self.logger.info(f"Starting parallel analysis with models: {model_keys}")
|
||||
start_time = time.time()
|
||||
|
||||
# Validate model keys
|
||||
valid_model_keys = []
|
||||
for model_key in model_keys:
|
||||
try:
|
||||
provider = self.get_provider(model_key)
|
||||
if provider.validate_config():
|
||||
valid_model_keys.append(model_key)
|
||||
else:
|
||||
self.logger.warning(f"Skipping {model_key} due to configuration issues")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to validate {model_key}: {e}")
|
||||
|
||||
if len(valid_model_keys) == 0:
|
||||
raise ValueError("No valid models available for analysis")
|
||||
|
||||
if len(valid_model_keys) < minimum_success_threshold:
|
||||
self.logger.warning(
|
||||
f"Only {len(valid_model_keys)} valid models, but minimum threshold is {minimum_success_threshold}"
|
||||
)
|
||||
|
||||
# Create tasks for parallel execution
|
||||
tasks = []
|
||||
for model_key in valid_model_keys:
|
||||
provider = self.get_provider(model_key)
|
||||
task = asyncio.create_task(
|
||||
self._execute_with_provider(provider, model_key, messages, schema, on_model_event)
|
||||
)
|
||||
tasks.append((model_key, task))
|
||||
|
||||
# Execute all tasks in parallel using asyncio.gather
|
||||
results = []
|
||||
successful_responses = []
|
||||
failed_responses = []
|
||||
|
||||
# Await all tasks simultaneously
|
||||
task_results = await asyncio.gather(*[task for _, task in tasks], return_exceptions=True)
|
||||
|
||||
# Process results
|
||||
for i, (model_key, task) in enumerate(tasks):
|
||||
result = task_results[i]
|
||||
|
||||
if isinstance(result, Exception):
|
||||
self.logger.error(f"Task for {model_key} raised exception: {result}")
|
||||
failed_responses.append((model_key, str(result)))
|
||||
else:
|
||||
response = result
|
||||
results.append((model_key, response))
|
||||
|
||||
if response.success:
|
||||
successful_responses.append(response)
|
||||
# Try to parse the response to count deliverables
|
||||
deliverable_count = self._count_deliverables_in_response(response.content)
|
||||
self.logger.info(f"{model_key} analysis completed successfully - found {deliverable_count} deliverables")
|
||||
else:
|
||||
failed_responses.append((model_key, response.error))
|
||||
self.logger.warning(f"{model_key} analysis failed: {response.error}")
|
||||
|
||||
total_time = time.time() - start_time
|
||||
|
||||
# Check if we meet minimum success threshold
|
||||
if len(successful_responses) < minimum_success_threshold:
|
||||
raise RuntimeError(
|
||||
f"Only {len(successful_responses)} models succeeded, "
|
||||
f"but minimum threshold is {minimum_success_threshold}"
|
||||
)
|
||||
|
||||
# Compile metadata
|
||||
metadata = {
|
||||
'total_models_requested': len(model_keys),
|
||||
'valid_models': len(valid_model_keys),
|
||||
'successful_models': len(successful_responses),
|
||||
'failed_models': len(failed_responses),
|
||||
'total_processing_time': total_time,
|
||||
'model_results': {
|
||||
model_key: {
|
||||
'success': response.success,
|
||||
'processing_time': response.processing_time,
|
||||
'tokens_used': response.token_usage.get_total(),
|
||||
'provider': response.provider,
|
||||
'model': response.model_used,
|
||||
'error': response.error
|
||||
} for model_key, response in results
|
||||
},
|
||||
'failures': failed_responses
|
||||
}
|
||||
|
||||
self.logger.info(
|
||||
f"Parallel analysis completed - {len(successful_responses)}/{len(valid_model_keys)} "
|
||||
f"models succeeded in {total_time:.2f}s"
|
||||
)
|
||||
|
||||
return successful_responses, metadata
|
||||
|
||||
def _count_deliverables_in_response(self, content: str) -> int:
|
||||
"""Count the number of deliverables in a model's JSON response"""
|
||||
try:
|
||||
import json
|
||||
data = json.loads(content)
|
||||
if isinstance(data, dict) and 'assets' in data:
|
||||
return len(data['assets'])
|
||||
return 0
|
||||
except (json.JSONDecodeError, KeyError, TypeError):
|
||||
return 0
|
||||
|
||||
async def _execute_with_provider(
|
||||
self,
|
||||
provider: BaseLLMProvider,
|
||||
model_key: str,
|
||||
messages: List[Dict[str, str]],
|
||||
schema: Optional[Dict[str, Any]] = None,
|
||||
on_model_event: Optional[callable] = None
|
||||
) -> LLMResponse:
|
||||
"""Execute analysis with a single provider"""
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
self.logger.debug(f"Starting analysis with {model_key}")
|
||||
|
||||
# Notify start event
|
||||
if on_model_event:
|
||||
await on_model_event(model_key, 'start', {
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
})
|
||||
|
||||
start_time = time.time()
|
||||
response = await provider.generate_response(messages, schema)
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
# Calculate cost if possible
|
||||
cost = 0.0
|
||||
try:
|
||||
cost = provider.estimate_cost(
|
||||
response.token_usage.input_tokens,
|
||||
response.token_usage.output_tokens,
|
||||
response.token_usage.cached_input_tokens
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Notify success event
|
||||
if on_model_event:
|
||||
await on_model_event(model_key, 'end', {
|
||||
'response': response,
|
||||
'cost': cost,
|
||||
'processing_time': processing_time,
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Provider {model_key} execution failed: {e}")
|
||||
|
||||
# Notify error event
|
||||
if on_model_event:
|
||||
await on_model_event(model_key, 'end', {
|
||||
'error': str(e),
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
})
|
||||
|
||||
return LLMResponse(
|
||||
content="",
|
||||
raw_response=None,
|
||||
token_usage=TokenUsage(),
|
||||
model_used=model_key,
|
||||
provider=provider.get_provider_name(),
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
def estimate_total_cost(self, model_keys: List[str], estimated_input_tokens: int, estimated_output_tokens: int) -> Dict[str, float]:
|
||||
"""Estimate total cost for all models"""
|
||||
cost_breakdown = {}
|
||||
total_cost = 0.0
|
||||
|
||||
for model_key in model_keys:
|
||||
try:
|
||||
provider = self.get_provider(model_key)
|
||||
model_cost = provider.estimate_cost(estimated_input_tokens, estimated_output_tokens)
|
||||
cost_breakdown[model_key] = model_cost
|
||||
total_cost += model_cost
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not estimate cost for {model_key}: {e}")
|
||||
cost_breakdown[model_key] = 0.0
|
||||
|
||||
cost_breakdown['total'] = total_cost
|
||||
return cost_breakdown
|
||||
|
||||
def get_aggregated_token_usage(self, responses: List[LLMResponse]) -> TokenUsage:
|
||||
"""Aggregate token usage from multiple responses"""
|
||||
total_usage = TokenUsage()
|
||||
|
||||
for response in responses:
|
||||
total_usage.input_tokens += response.token_usage.input_tokens
|
||||
total_usage.output_tokens += response.token_usage.output_tokens
|
||||
total_usage.cached_input_tokens += response.token_usage.cached_input_tokens
|
||||
|
||||
return total_usage
|
||||
|
||||
def get_actual_cost_breakdown(self, responses: List[LLMResponse]) -> Dict[str, float]:
|
||||
"""Calculate actual costs from responses"""
|
||||
cost_breakdown = {}
|
||||
total_cost = 0.0
|
||||
|
||||
for response in responses:
|
||||
try:
|
||||
provider = self.providers.get(response.model_used)
|
||||
if provider:
|
||||
cost = provider.estimate_cost(
|
||||
response.token_usage.input_tokens,
|
||||
response.token_usage.output_tokens,
|
||||
response.token_usage.cached_input_tokens
|
||||
)
|
||||
cost_breakdown[response.model_used] = cost
|
||||
total_cost += cost
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not calculate cost for {response.model_used}: {e}")
|
||||
|
||||
cost_breakdown['total'] = total_cost
|
||||
return cost_breakdown
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,712 +0,0 @@
|
|||
# Enhanced Brief Processing System - Production Deployment Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide provides complete instructions for deploying the Enhanced Brief Processing System on an Ubuntu web server. The system consists of a React frontend and Python backend with real-time WebSocket communication.
|
||||
|
||||
**Architecture:** React Frontend → Apache Proxy → Python Backend (Quart/WebSocket) → AI Services
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Requirements
|
||||
- **OS:** Ubuntu 20.04 LTS or newer
|
||||
- **CPU:** 4+ cores (8+ recommended for production)
|
||||
- **RAM:** 8GB minimum (16GB+ recommended)
|
||||
- **Storage:** 100GB+ SSD with high IOPS
|
||||
- **Network:** Stable internet connection for AI API calls
|
||||
|
||||
### Required Software
|
||||
```bash
|
||||
# Update system packages
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Install essential packages
|
||||
sudo apt install -y python3.11 python3.11-venv python3.11-dev python3-pip
|
||||
sudo apt install -y nodejs npm apache2 git curl wget unzip
|
||||
sudo apt install -y build-essential libffi-dev libssl-dev
|
||||
|
||||
# Enable Apache modules
|
||||
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite ssl headers
|
||||
```
|
||||
|
||||
### Required API Keys
|
||||
Before deployment, ensure you have:
|
||||
- **OpenAI API Key** (for GPT-5 access)
|
||||
- **Anthropic API Key** (for Claude models)
|
||||
- **Google AI API Key** (for Gemini 2.5 Pro)
|
||||
- **LlamaCloud API Key** (for document parsing)
|
||||
- **Microsoft Azure AD App Registration** (for production authentication)
|
||||
|
||||
## Step 1: Prepare Deployment Files
|
||||
|
||||
### 1.1 Create Deployment Directory
|
||||
```bash
|
||||
# Create deployment directory
|
||||
sudo mkdir -p /var/www/html/brief-extractor
|
||||
sudo chown -R $USER:www-data /var/www/html/brief-extractor
|
||||
sudo chmod -R 755 /var/www/html/brief-extractor
|
||||
```
|
||||
|
||||
### 1.2 Build Frontend
|
||||
On your development machine:
|
||||
```bash
|
||||
# Navigate to frontend directory
|
||||
cd frontend
|
||||
|
||||
# Set production environment
|
||||
echo "VITE_API_BASE_URL=/api" > .env.production
|
||||
|
||||
# Build for production
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
# The built files will be in frontend/dist/
|
||||
```
|
||||
|
||||
### 1.3 Copy Files to Server
|
||||
|
||||
**Frontend Files (copy contents of `frontend/dist/` to server):**
|
||||
```bash
|
||||
# On your local machine, create deployment package
|
||||
cd frontend/dist
|
||||
tar -czf frontend-dist.tar.gz *
|
||||
|
||||
# Copy to server
|
||||
scp frontend-dist.tar.gz user@your-server:/tmp/
|
||||
|
||||
# On server, extract frontend files
|
||||
cd /var/www/html/brief-extractor
|
||||
sudo tar -xzf /tmp/frontend-dist.tar.gz
|
||||
sudo chown -R www-data:www-data /var/www/html/brief-extractor
|
||||
```
|
||||
|
||||
**Backend Files (copy these directories/files to server):**
|
||||
```bash
|
||||
# Create backend directory
|
||||
sudo mkdir -p /var/www/html/brief-extractor/backend
|
||||
cd /var/www/html/brief-extractor/backend
|
||||
|
||||
# Copy these files/directories from your project:
|
||||
# - core/ (entire directory)
|
||||
# - server/ (entire directory)
|
||||
# - prompts/ (entire directory)
|
||||
# - run_server.py
|
||||
# - requirements_enhanced.txt
|
||||
# - server_requirements.txt
|
||||
# - .env.example (rename to .env and configure)
|
||||
|
||||
# Example rsync command (adjust paths as needed):
|
||||
rsync -av --exclude='venv' --exclude='node_modules' --exclude='frontend' --exclude='output' --exclude='__pycache__' /path/to/your/project/ user@your-server:/var/www/html/brief-extractor/backend/
|
||||
```
|
||||
|
||||
## Step 2: Backend Setup
|
||||
|
||||
### 2.1 Create Python Virtual Environment
|
||||
```bash
|
||||
cd /var/www/html/brief-extractor/backend
|
||||
|
||||
# Create virtual environment
|
||||
python3.11 -m venv venv
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Upgrade pip
|
||||
pip install --upgrade pip setuptools wheel
|
||||
```
|
||||
|
||||
### 2.2 Install Python Dependencies
|
||||
```bash
|
||||
# Install core dependencies
|
||||
pip install -r requirements_enhanced.txt
|
||||
|
||||
# Install web server dependencies
|
||||
pip install -r server_requirements.txt
|
||||
|
||||
# Verify installation
|
||||
python -c "import quart, anthropic, openai; print('Dependencies installed successfully')"
|
||||
```
|
||||
|
||||
### 2.3 Configure Environment Variables
|
||||
```bash
|
||||
# Copy and edit environment file
|
||||
cp .env.example .env
|
||||
sudo nano .env
|
||||
```
|
||||
|
||||
**Production .env Configuration:**
|
||||
```env
|
||||
# Production Mode Settings
|
||||
DEV_MODE=false
|
||||
DEBUG=false
|
||||
ENVIRONMENT=production
|
||||
|
||||
# Server Configuration
|
||||
SERVER_HOST=127.0.0.1
|
||||
SERVER_PORT=8000
|
||||
SERVER_WORKERS=4
|
||||
|
||||
# Security Settings
|
||||
SESSION_SECRET=your-cryptographically-strong-secret-here-min-32-chars
|
||||
SECURE_COOKIES=true
|
||||
CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
|
||||
|
||||
# API Keys (REQUIRED)
|
||||
OPENAI_API_KEY=sk-your-openai-api-key-here
|
||||
ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key-here
|
||||
GOOGLE_API_KEY=AIzaSy-your-google-api-key-here
|
||||
LLAMACLOUD_API_KEY=llx-your-llamacloud-api-key-here
|
||||
|
||||
# Model Configuration
|
||||
OPENAI_REASONING_EFFORT=medium
|
||||
ANTHROPIC_MAX_TOKENS=64000
|
||||
GOOGLE_MAX_OUTPUT_TOKENS=100000
|
||||
|
||||
# Processing Settings
|
||||
DEFAULT_PRIMARY_MODELS=openai-gpt5,anthropic-sonnet4,google-gemini25
|
||||
DEFAULT_CONSOLIDATION_MODEL=openai-gpt5
|
||||
MAX_PROCESSING_COST_USD=10.00
|
||||
MAX_CONCURRENT_JOBS=3
|
||||
MAX_UPLOAD_SIZE_MB=200
|
||||
FILE_RETENTION_HOURS=24
|
||||
|
||||
# Microsoft Azure AD (Production Authentication)
|
||||
MSAL_CLIENT_ID=your-azure-app-client-id
|
||||
MSAL_CLIENT_SECRET=your-azure-app-client-secret
|
||||
MSAL_TENANT_ID=your-azure-tenant-id
|
||||
MSAL_AUTHORITY=https://login.microsoftonline.com/your-tenant-id
|
||||
MSAL_REDIRECT_URI=https://yourdomain.com/auth/callback
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FILE=/var/log/brief-extractor/app.log
|
||||
```
|
||||
|
||||
### 2.4 Create Required Directories
|
||||
```bash
|
||||
# Create data directories
|
||||
sudo mkdir -p /var/www/html/brief-extractor/backend/server/data/{uploads,outputs}
|
||||
sudo mkdir -p /var/log/brief-extractor
|
||||
|
||||
# Set permissions
|
||||
sudo chown -R $USER:www-data /var/www/html/brief-extractor/backend/server/data
|
||||
sudo chmod -R 775 /var/www/html/brief-extractor/backend/server/data
|
||||
sudo chown -R $USER:adm /var/log/brief-extractor
|
||||
sudo chmod -R 755 /var/log/brief-extractor
|
||||
```
|
||||
|
||||
### 2.5 Test Backend Installation
|
||||
```bash
|
||||
# Test the backend can start
|
||||
cd /var/www/html/brief-extractor/backend
|
||||
source venv/bin/activate
|
||||
python -c "from server.app import create_app; print('Backend setup successful')"
|
||||
```
|
||||
|
||||
## Step 3: Create Systemd Service
|
||||
|
||||
### 3.1 Create Service File
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/brief-extractor.service
|
||||
```
|
||||
|
||||
**Service Configuration:**
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Enhanced Brief Processing System Backend
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/var/www/html/brief-extractor/backend
|
||||
Environment=PATH=/var/www/html/brief-extractor/backend/venv/bin
|
||||
EnvironmentFile=/var/www/html/brief-extractor/backend/.env
|
||||
ExecStart=/var/www/html/brief-extractor/backend/venv/bin/python run_server.py
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
KillMode=mixed
|
||||
TimeoutStopSec=5
|
||||
PrivateTmp=true
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths=/var/www/html/brief-extractor/backend/server/data /var/log/brief-extractor /tmp
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65536
|
||||
LimitNPROC=4096
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### 3.2 Configure Service Permissions
|
||||
```bash
|
||||
# Set proper ownership
|
||||
sudo chown -R www-data:www-data /var/www/html/brief-extractor/backend
|
||||
sudo chmod +x /var/www/html/brief-extractor/backend/run_server.py
|
||||
|
||||
# Reload systemd
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Enable service to start on boot
|
||||
sudo systemctl enable brief-extractor.service
|
||||
```
|
||||
|
||||
## Step 4: Configure Apache Reverse Proxy
|
||||
|
||||
### 4.1 Configure Apache
|
||||
```bash
|
||||
sudo nano /etc/apache2/apache2.conf
|
||||
```
|
||||
|
||||
**Add this configuration to `/etc/apache2/apache2.conf`:**
|
||||
```apache
|
||||
# Brief Extractor Virtual Host Configuration
|
||||
<VirtualHost *:80>
|
||||
ServerName yourdomain.com
|
||||
ServerAlias www.yourdomain.com
|
||||
DocumentRoot /var/www/html/brief-extractor
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTPS} off
|
||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName yourdomain.com
|
||||
ServerAlias www.yourdomain.com
|
||||
DocumentRoot /var/www/html/brief-extractor
|
||||
|
||||
# SSL Configuration (configure with your certificates)
|
||||
SSLEngine on
|
||||
SSLCertificateFile /path/to/your/certificate.crt
|
||||
SSLCertificateKeyFile /path/to/your/private.key
|
||||
SSLCertificateChainFile /path/to/your/chain.crt
|
||||
|
||||
# Security Headers
|
||||
Header always set X-Content-Type-Options nosniff
|
||||
Header always set X-Frame-Options DENY
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
|
||||
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' wss://yourdomain.com https://api.openai.com https://api.anthropic.com https://generativelanguage.googleapis.com"
|
||||
|
||||
# Static files (React frontend)
|
||||
<Directory /var/www/html/brief-extractor>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
|
||||
# Handle React Router (SPA)
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteRule ^index\.html$ - [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} !^/api
|
||||
RewriteCond %{REQUEST_URI} !^/ws
|
||||
RewriteRule . /index.html [L]
|
||||
</Directory>
|
||||
|
||||
# Proxy API requests to backend
|
||||
ProxyPreserveHost On
|
||||
ProxyRequests Off
|
||||
|
||||
# API endpoints
|
||||
ProxyPass /api/ http://127.0.0.1:8000/api/
|
||||
ProxyPassReverse /api/ http://127.0.0.1:8000/api/
|
||||
|
||||
# Health check endpoint
|
||||
ProxyPass /health http://127.0.0.1:8000/health
|
||||
ProxyPassReverse /health http://127.0.0.1:8000/health
|
||||
|
||||
# WebSocket proxy
|
||||
ProxyPass /ws/ ws://127.0.0.1:8000/ws/
|
||||
ProxyPassReverse /ws/ ws://127.0.0.1:8000/ws/
|
||||
|
||||
# Logging
|
||||
ErrorLog ${APACHE_LOG_DIR}/brief-extractor_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/brief-extractor_access.log combined
|
||||
|
||||
# Optional: Rate limiting (requires mod_evasive)
|
||||
# DOSHashTableSize 32768
|
||||
# DOSPageCount 10
|
||||
# DOSPageInterval 2
|
||||
# DOSSiteCount 50
|
||||
# DOSSiteInterval 2
|
||||
# DOSBlockingPeriod 600
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
### 4.2 Test and Restart Apache
|
||||
```bash
|
||||
# Test Apache configuration
|
||||
sudo apache2ctl configtest
|
||||
|
||||
# Restart Apache
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
## Step 5: SSL/TLS Certificate Setup
|
||||
|
||||
### 5.1 Using Let's Encrypt (Recommended)
|
||||
```bash
|
||||
# Install Certbot
|
||||
sudo apt install -y certbot python3-certbot-apache
|
||||
|
||||
# Obtain SSL certificate
|
||||
sudo certbot --apache -d yourdomain.com -d www.yourdomain.com
|
||||
|
||||
# Test automatic renewal
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
### 5.2 Configure Auto-renewal
|
||||
```bash
|
||||
# Add cron job for certificate renewal
|
||||
sudo crontab -e
|
||||
|
||||
# Add this line:
|
||||
0 12 * * * /usr/bin/certbot renew --quiet
|
||||
```
|
||||
|
||||
## Step 6: Frontend Configuration for Production
|
||||
|
||||
### 6.1 Update Frontend Build Configuration
|
||||
If you need to rebuild the frontend for production, update the environment:
|
||||
|
||||
```bash
|
||||
# In frontend/.env.production
|
||||
VITE_API_BASE_URL=https://yourdomain.com/api
|
||||
VITE_WS_URL=wss://yourdomain.com/ws
|
||||
VITE_MSAL_CLIENT_ID=your-azure-app-client-id
|
||||
VITE_MSAL_TENANT_ID=your-azure-tenant-id
|
||||
VITE_MSAL_AUTHORITY=https://login.microsoftonline.com/your-tenant-id
|
||||
VITE_MSAL_REDIRECT_URI=https://yourdomain.com/auth/callback
|
||||
```
|
||||
|
||||
### 6.2 Rebuild and Deploy Frontend (if needed)
|
||||
```bash
|
||||
# Rebuild frontend
|
||||
cd frontend
|
||||
npm run build
|
||||
|
||||
# Copy updated files to server
|
||||
rsync -av dist/ user@your-server:/var/www/html/brief-extractor/
|
||||
```
|
||||
|
||||
## Step 7: Start and Monitor Services
|
||||
|
||||
### 7.1 Start the Backend Service
|
||||
```bash
|
||||
# Start the service
|
||||
sudo systemctl start brief-extractor.service
|
||||
|
||||
# Check service status
|
||||
sudo systemctl status brief-extractor.service
|
||||
|
||||
# Monitor logs in real-time
|
||||
sudo journalctl -u brief-extractor.service -f
|
||||
|
||||
# Check if service is listening on port 8000
|
||||
sudo ss -tulpn | grep :8000
|
||||
```
|
||||
|
||||
### 7.2 Verify System Health
|
||||
```bash
|
||||
# Test backend health endpoint
|
||||
curl -k https://yourdomain.com/health
|
||||
|
||||
# Test API endpoint
|
||||
curl -k https://yourdomain.com/api/config/system
|
||||
|
||||
# Check Apache status
|
||||
sudo systemctl status apache2
|
||||
|
||||
# View Apache logs
|
||||
sudo tail -f /var/log/apache2/brief-extractor_access.log
|
||||
sudo tail -f /var/log/apache2/brief-extractor_error.log
|
||||
```
|
||||
|
||||
## Step 8: Testing and Validation
|
||||
|
||||
### 8.1 Backend Testing
|
||||
```bash
|
||||
# Test Python backend directly
|
||||
cd /var/www/html/brief-extractor/backend
|
||||
source venv/bin/activate
|
||||
python -c "
|
||||
import asyncio
|
||||
from server.app import create_app
|
||||
print('Backend imports successful')
|
||||
"
|
||||
|
||||
# Test environment variables
|
||||
python -c "
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
print('Environment loaded:', bool(os.getenv('OPENAI_API_KEY')))
|
||||
"
|
||||
```
|
||||
|
||||
### 8.2 Frontend Access Testing
|
||||
1. **Open browser** and navigate to `https://yourdomain.com`
|
||||
2. **Check console** for any JavaScript errors
|
||||
3. **Test authentication** (login/logout flow)
|
||||
4. **Upload a test document** to verify full workflow
|
||||
5. **Monitor WebSocket connection** in browser developer tools
|
||||
|
||||
### 8.3 End-to-End Testing
|
||||
1. **Authentication:** Verify MSAL login works
|
||||
2. **File Upload:** Test document upload functionality
|
||||
3. **Processing:** Upload a test brief and verify processing
|
||||
4. **Real-time Updates:** Check WebSocket updates during processing
|
||||
5. **Download Results:** Verify CSV download works
|
||||
6. **Cleanup:** Test job deletion
|
||||
|
||||
## Step 9: Production Monitoring and Maintenance
|
||||
|
||||
### 9.1 Log Monitoring
|
||||
```bash
|
||||
# Backend application logs
|
||||
sudo journalctl -u brief-extractor.service -f
|
||||
|
||||
# Apache access logs
|
||||
sudo tail -f /var/log/apache2/brief-extractor_access.log
|
||||
|
||||
# Apache error logs
|
||||
sudo tail -f /var/log/apache2/brief-extractor_error.log
|
||||
|
||||
# System logs
|
||||
sudo tail -f /var/log/syslog
|
||||
```
|
||||
|
||||
### 9.2 Resource Monitoring
|
||||
```bash
|
||||
# Monitor system resources
|
||||
htop
|
||||
|
||||
# Check disk usage
|
||||
df -h
|
||||
|
||||
# Monitor network connections
|
||||
sudo ss -tulpn | grep -E "(8000|80|443)"
|
||||
|
||||
# Check memory usage
|
||||
free -h
|
||||
```
|
||||
|
||||
### 9.3 Automated Cleanup
|
||||
Create a cleanup script for old files:
|
||||
```bash
|
||||
sudo nano /usr/local/bin/brief-extractor-cleanup.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Cleanup old uploaded files and results
|
||||
|
||||
UPLOAD_DIR="/var/www/html/brief-extractor/backend/server/data/uploads"
|
||||
OUTPUT_DIR="/var/www/html/brief-extractor/backend/server/data/outputs"
|
||||
|
||||
# Remove files older than 24 hours
|
||||
find "$UPLOAD_DIR" -type f -mtime +1 -delete
|
||||
find "$OUTPUT_DIR" -type f -mtime +1 -delete
|
||||
|
||||
# Log cleanup
|
||||
echo "$(date): Cleaned up old files" >> /var/log/brief-extractor/cleanup.log
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo chmod +x /usr/local/bin/brief-extractor-cleanup.sh
|
||||
|
||||
# Add to cron
|
||||
sudo crontab -e
|
||||
# Add: 0 2 * * * /usr/local/bin/brief-extractor-cleanup.sh
|
||||
```
|
||||
|
||||
## Step 10: Backup and Disaster Recovery
|
||||
|
||||
### 10.1 Backup Strategy
|
||||
```bash
|
||||
# Create backup script
|
||||
sudo nano /usr/local/bin/brief-extractor-backup.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/var/backups/brief-extractor"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Backup application files
|
||||
tar -czf "$BACKUP_DIR/app_$DATE.tar.gz" -C /var/www/html brief-extractor
|
||||
|
||||
# Backup configuration
|
||||
cp /etc/systemd/system/brief-extractor.service "$BACKUP_DIR/service_$DATE"
|
||||
cp /etc/apache2/apache2.conf "$BACKUP_DIR/apache_$DATE.conf"
|
||||
|
||||
# Keep only last 7 days of backups
|
||||
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +7 -delete
|
||||
|
||||
echo "$(date): Backup completed" >> /var/log/brief-extractor/backup.log
|
||||
```
|
||||
|
||||
### 10.2 Health Checks
|
||||
```bash
|
||||
# Create health check script
|
||||
sudo nano /usr/local/bin/brief-extractor-healthcheck.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Health check script
|
||||
|
||||
SERVICE_NAME="brief-extractor"
|
||||
HEALTH_URL="https://yourdomain.com/health"
|
||||
LOG_FILE="/var/log/brief-extractor/healthcheck.log"
|
||||
|
||||
# Check service status
|
||||
if ! systemctl is-active --quiet $SERVICE_NAME; then
|
||||
echo "$(date): Service $SERVICE_NAME is not running" >> $LOG_FILE
|
||||
systemctl restart $SERVICE_NAME
|
||||
fi
|
||||
|
||||
# Check HTTP health endpoint
|
||||
if ! curl -sf $HEALTH_URL > /dev/null; then
|
||||
echo "$(date): Health check failed for $HEALTH_URL" >> $LOG_FILE
|
||||
fi
|
||||
```
|
||||
|
||||
## Troubleshooting Guide
|
||||
|
||||
### Common Issues and Solutions
|
||||
|
||||
1. **Service Won't Start:**
|
||||
```bash
|
||||
# Check service logs
|
||||
sudo journalctl -u brief-extractor.service -n 50
|
||||
|
||||
# Check file permissions
|
||||
sudo chown -R www-data:www-data /var/www/html/brief-extractor/backend
|
||||
|
||||
# Verify Python virtual environment
|
||||
cd /var/www/html/brief-extractor/backend
|
||||
source venv/bin/activate
|
||||
python run_server.py
|
||||
```
|
||||
|
||||
2. **Frontend Not Loading:**
|
||||
```bash
|
||||
# Check Apache configuration
|
||||
sudo apache2ctl configtest
|
||||
|
||||
# Verify file permissions
|
||||
sudo chown -R www-data:www-data /var/www/html/brief-extractor
|
||||
|
||||
# Check SSL certificate
|
||||
sudo certbot certificates
|
||||
```
|
||||
|
||||
3. **WebSocket Connection Issues:**
|
||||
```bash
|
||||
# Check proxy configuration
|
||||
# Ensure mod_proxy_wstunnel is enabled
|
||||
sudo a2enmod proxy_wstunnel
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
4. **Authentication Failures:**
|
||||
```bash
|
||||
# Verify MSAL configuration
|
||||
# Check Azure AD app registration settings
|
||||
# Verify redirect URIs match
|
||||
```
|
||||
|
||||
5. **File Upload Issues:**
|
||||
```bash
|
||||
# Check upload directory permissions
|
||||
sudo chmod -R 775 /var/www/html/brief-extractor/backend/server/data/uploads
|
||||
|
||||
# Check disk space
|
||||
df -h
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
1. **Enable Gzip Compression:**
|
||||
```apache
|
||||
# Add to Apache config
|
||||
LoadModule deflate_module modules/mod_deflate.so
|
||||
<Location />
|
||||
SetOutputFilter DEFLATE
|
||||
</Location>
|
||||
```
|
||||
|
||||
2. **Cache Static Assets:**
|
||||
```apache
|
||||
# Add cache headers for static files
|
||||
<LocationMatch "\.(css|js|png|jpg|jpeg|gif|ico|svg)$">
|
||||
ExpiresActive On
|
||||
ExpiresDefault "access plus 1 month"
|
||||
</LocationMatch>
|
||||
```
|
||||
|
||||
3. **Monitor Resource Usage:**
|
||||
- Install monitoring tools like htop, iotop
|
||||
- Set up log rotation to prevent disk space issues
|
||||
- Consider upgrading server resources based on usage patterns
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### 10.1 System Hardening
|
||||
```bash
|
||||
# Configure firewall
|
||||
sudo ufw enable
|
||||
sudo ufw allow 22/tcp # SSH
|
||||
sudo ufw allow 80/tcp # HTTP
|
||||
sudo ufw allow 443/tcp # HTTPS
|
||||
|
||||
# Disable unused services
|
||||
sudo systemctl disable apache2-doc
|
||||
|
||||
# Regular security updates
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
```
|
||||
|
||||
### 10.2 Application Security
|
||||
- **API Keys:** Ensure all API keys are kept secure and rotated regularly
|
||||
- **File Validation:** The system includes file type validation
|
||||
- **Authentication:** MSAL provides secure authentication for production
|
||||
- **HTTPS:** Always use HTTPS in production
|
||||
- **CORS:** Configure CORS policies for your domain only
|
||||
|
||||
## Conclusion
|
||||
|
||||
After completing these steps, your Enhanced Brief Processing System should be fully deployed and operational on your Ubuntu server. The system will:
|
||||
|
||||
- ✅ Serve the React frontend via Apache
|
||||
- ✅ Proxy API calls to the Python backend
|
||||
- ✅ Handle WebSocket connections for real-time updates
|
||||
- ✅ Process documents using multiple AI models
|
||||
- ✅ Provide secure authentication via Microsoft Azure AD
|
||||
- ✅ Automatically start on system boot
|
||||
- ✅ Include monitoring and logging capabilities
|
||||
|
||||
**Next Steps:**
|
||||
- Configure monitoring and alerting
|
||||
- Set up automated backups
|
||||
- Test disaster recovery procedures
|
||||
- Monitor costs and usage of AI services
|
||||
- Consider scaling strategies for high-volume usage
|
||||
|
||||
For ongoing maintenance, monitor the logs regularly and keep the system updated with security patches.
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.backend
|
||||
ports:
|
||||
- "8000:8000"
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./server/data:/app/server/data
|
||||
- ./prompts:/app/prompts
|
||||
- ./core:/app/core
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- brief-extractor
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:80"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- VITE_API_URL=http://localhost:8000/api
|
||||
- VITE_WS_URL=ws://localhost:8000
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
networks:
|
||||
- brief-extractor
|
||||
|
||||
# Optional: Reverse proxy for production
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
depends_on:
|
||||
- backend
|
||||
- frontend
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- production
|
||||
networks:
|
||||
- brief-extractor
|
||||
|
||||
networks:
|
||||
brief-extractor:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
uploads:
|
||||
outputs:
|
||||
Binary file not shown.
|
|
@ -1,650 +0,0 @@
|
|||
# Adidas Brief Extraction System - Technical Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Adidas Brief Extraction System is a sophisticated CLI-based document analysis tool that leverages OpenAI's GPT-5 model with reasoning capabilities to extract structured marketing asset information from complex creative briefs, presentations, and technical documents. The system employs a multi-pass analysis pipeline with comprehensive validation to achieve superior accuracy in asset extraction.
|
||||
|
||||
### Key Features
|
||||
- **GPT-5 Integration**: Uses OpenAI's latest GPT-5 model with configurable reasoning effort
|
||||
- **LlamaParser Integration**: Advanced document preprocessing for optimal content extraction
|
||||
- **Multi-Pass Analysis**: Multi-perspective analysis with cross-validation
|
||||
- **Cost Tracking**: Comprehensive token usage and cost management
|
||||
- **Schema Validation**: Pydantic-based data models with JSON schema validation
|
||||
- **Comprehensive Logging**: Detailed processing logs with error handling
|
||||
|
||||
## System Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Input Layer"
|
||||
A[Document Files] --> B{Document Classifier}
|
||||
B --> C[PowerPoint]
|
||||
B --> D[Word]
|
||||
B --> E[PDF]
|
||||
B --> F[Excel]
|
||||
end
|
||||
|
||||
subgraph "Preprocessing Layer"
|
||||
C --> G[LlamaParser Cloud]
|
||||
D --> G
|
||||
E --> G
|
||||
F --> G
|
||||
G --> H[Structured Markdown]
|
||||
end
|
||||
|
||||
subgraph "AI Processing Layer"
|
||||
H --> I[DocumentAnalyzer]
|
||||
I --> J[Multi-Perspective Analysis]
|
||||
J --> K[Cross-Validation & Enhancement]
|
||||
K --> L[Asset Extraction Result]
|
||||
end
|
||||
|
||||
subgraph "Output Layer"
|
||||
L --> M[CSV Generator]
|
||||
M --> N[Structured CSV Output]
|
||||
end
|
||||
|
||||
subgraph "External Services"
|
||||
O[OpenAI GPT-5 API] --> J
|
||||
O --> K
|
||||
P[LlamaCloud API] --> G
|
||||
end
|
||||
|
||||
subgraph "Monitoring & Logging"
|
||||
Q[Token Usage Tracker] --> I
|
||||
R[Cost Calculator] --> Q
|
||||
S[Processing Logger] --> I
|
||||
end
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. DocumentAnalyzer Class (`process_brief_enhanced.py:216`)
|
||||
|
||||
The main orchestrator that manages the entire document processing pipeline.
|
||||
|
||||
**Key Responsibilities:**
|
||||
- Model configuration and API client setup
|
||||
- Document type classification
|
||||
- Multi-pass analysis coordination
|
||||
- Token usage tracking
|
||||
- Error handling and logging
|
||||
|
||||
**Configuration:**
|
||||
```python
|
||||
analyzer = DocumentAnalyzer(
|
||||
model_name='gpt-5', # OpenAI model
|
||||
reasoning_effort='medium' # high, medium, low, minimal
|
||||
)
|
||||
```
|
||||
|
||||
### 2. LlamaParser Integration (`process_brief_enhanced.py:278-325`)
|
||||
|
||||
Advanced document preprocessing using LlamaCloud services for optimal content extraction.
|
||||
|
||||
**Configuration Parameters:**
|
||||
```python
|
||||
parser = LlamaParse(
|
||||
api_key=LLAMACLOUD_API_KEY,
|
||||
parse_mode="parse_page_with_agent", # Agent-based parsing
|
||||
model="openai-gpt-5", # Using GPT-5 for parsing
|
||||
high_res_ocr=True, # High-resolution OCR
|
||||
adaptive_long_table=True, # Smart table handling
|
||||
outlined_table_extraction=True, # Table structure detection
|
||||
output_tables_as_HTML=True, # HTML table output
|
||||
page_separator="\n\n---\n\n" # Page separation marker
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Data Models
|
||||
|
||||
#### MarketingAsset (Pydantic Model - `process_brief_enhanced.py:50`)
|
||||
```python
|
||||
class MarketingAsset(BaseModel):
|
||||
title: str
|
||||
status: Optional[str] = ""
|
||||
category: Optional[str] = ""
|
||||
media: Optional[str] = ""
|
||||
asset_type: Optional[str] = ""
|
||||
brand_identifier: Optional[str] = ""
|
||||
format: Optional[str] = ""
|
||||
review_date: Optional[str] = ""
|
||||
live_date: Optional[str] = ""
|
||||
end_date: Optional[str] = ""
|
||||
reference_material: Optional[str] = ""
|
||||
language: Optional[str] = ""
|
||||
country: Optional[str] = ""
|
||||
quantity: Optional[str] = "1"
|
||||
page_number: Optional[str] = ""
|
||||
section_context: Optional[str] = ""
|
||||
priority_level: Optional[str] = ""
|
||||
technical_requirements: Optional[str] = ""
|
||||
creative_direction: Optional[str] = ""
|
||||
approval_level: Optional[str] = ""
|
||||
```
|
||||
|
||||
#### TokenUsage (Cost Tracking - `process_brief_enhanced.py:164`)
|
||||
```python
|
||||
@dataclass
|
||||
class TokenUsage:
|
||||
input_tokens: int = 0
|
||||
cached_input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
|
||||
def calculate_cost(self, model_name: str) -> float:
|
||||
# GPT-5 Pricing (per 1M tokens)
|
||||
pricing = {
|
||||
'input': 2.50,
|
||||
'cached_input': 1.25,
|
||||
'output': 10.00
|
||||
}
|
||||
```
|
||||
|
||||
## Process Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CLI as CLI Interface
|
||||
participant DA as DocumentAnalyzer
|
||||
participant LP as LlamaParser
|
||||
participant GPT5 as OpenAI GPT-5
|
||||
participant CSV as CSV Generator
|
||||
participant LOG as Logger
|
||||
|
||||
CLI->>DA: Initialize (model_name, reasoning_effort)
|
||||
DA->>LOG: Setup logging
|
||||
CLI->>DA: process_document_multi_pass(filepath)
|
||||
|
||||
Note over DA: Stage 1: Document Classification
|
||||
DA->>DA: classify_document(filepath)
|
||||
DA->>LOG: Log document type
|
||||
|
||||
Note over DA: Stage 2: Content Extraction
|
||||
DA->>LP: _extract_document_content(filepath)
|
||||
LP->>LP: Parse with agent-based parsing
|
||||
LP->>DA: Return structured markdown
|
||||
DA->>LOG: Log content extraction success
|
||||
|
||||
Note over DA: Stage 3: Multi-Perspective Analysis
|
||||
DA->>DA: _perform_multi_perspective_analysis()
|
||||
DA->>DA: _load_prompt('multi_perspective_analysis')
|
||||
DA->>GPT5: responses.parse() with reasoning effort
|
||||
GPT5->>DA: Return structured assets
|
||||
DA->>DA: Track token usage
|
||||
DA->>LOG: Log analysis completion
|
||||
|
||||
Note over DA: Stage 4: Cross-Validation
|
||||
DA->>DA: _enhance_and_validate_results()
|
||||
DA->>DA: _load_prompt('validation_analysis')
|
||||
DA->>GPT5: responses.parse() for validation
|
||||
GPT5->>DA: Return additional/corrected assets
|
||||
DA->>DA: Merge results
|
||||
DA->>LOG: Log validation results
|
||||
|
||||
Note over DA: Stage 5: Output Generation
|
||||
DA->>CSV: Generate CSV output
|
||||
CSV->>CLI: Return output filepath
|
||||
DA->>LOG: Log cost summary
|
||||
CLI->>CLI: Display processing summary
|
||||
```
|
||||
|
||||
## Detailed Processing Stages
|
||||
|
||||
### Stage 1: Document Classification (`process_brief_enhanced.py:254`)
|
||||
|
||||
```python
|
||||
def classify_document(self, filepath: str) -> DocumentType:
|
||||
extension = os.path.splitext(filepath)[1].lower()
|
||||
|
||||
if extension in ['.ppt', '.pptx']:
|
||||
return DocumentType.POWERPOINT
|
||||
elif extension in ['.doc', '.docx']:
|
||||
return DocumentType.WORD
|
||||
elif extension == '.pdf':
|
||||
return DocumentType.PDF
|
||||
elif extension in ['.xls', '.xlsx']:
|
||||
return DocumentType.EXCEL
|
||||
else:
|
||||
return DocumentType.UNKNOWN
|
||||
```
|
||||
|
||||
### Stage 2: Content Extraction with LlamaParser
|
||||
|
||||
The system uses LlamaCloud's parsing service to convert complex documents into clean, structured markdown:
|
||||
|
||||
**Key Features:**
|
||||
- **Agent-based parsing**: Uses AI agents to understand document structure
|
||||
- **High-resolution OCR**: Extracts text from images and scanned documents
|
||||
- **Adaptive table handling**: Detects and preserves table structures
|
||||
- **Page separation**: Maintains document flow with clear page boundaries
|
||||
|
||||
### Stage 3: Multi-Perspective Analysis
|
||||
|
||||
Uses external prompt files for maintainable AI instructions:
|
||||
|
||||
**Prompt Loading System (`process_brief_enhanced.py:241`):**
|
||||
```python
|
||||
def _load_prompt(self, prompt_name: str) -> str:
|
||||
prompt_path = os.path.join(os.path.dirname(__file__), 'prompts', f'{prompt_name}.txt')
|
||||
with open(prompt_path, 'r', encoding='utf-8') as f:
|
||||
return f.read().strip()
|
||||
```
|
||||
|
||||
**Analysis Prompts:**
|
||||
- `multi_perspective_analysis.txt`: Comprehensive asset extraction rules
|
||||
- `system_multi_perspective.txt`: System message for analysis
|
||||
- `validation_analysis.txt`: Quality assurance and gap analysis
|
||||
- `system_validation.txt`: System message for validation
|
||||
|
||||
### Stage 4: Cross-Validation & Enhancement
|
||||
|
||||
Implements a two-pass validation system:
|
||||
1. **Initial Analysis**: Extracts assets from multiple professional perspectives
|
||||
2. **Validation Pass**: Reviews extraction completeness and accuracy
|
||||
3. **Gap Analysis**: Identifies missing or incorrectly extracted assets
|
||||
4. **Result Merging**: Combines and deduplicates findings
|
||||
|
||||
### Stage 5: CSV Output Generation
|
||||
|
||||
Generates structured CSV files with 20 standardized columns:
|
||||
|
||||
```python
|
||||
CSV_HEADERS = [
|
||||
'title', 'status', 'category', 'media', 'asset_type',
|
||||
'brand_identifier', 'format', 'review_date', 'live_date',
|
||||
'end_date', 'reference_material', 'language', 'country',
|
||||
'quantity', 'page_number', 'section_context', 'priority_level',
|
||||
'technical_requirements', 'creative_direction', 'approval_level'
|
||||
]
|
||||
```
|
||||
|
||||
## API Integration Details
|
||||
|
||||
### OpenAI GPT-5 Integration
|
||||
|
||||
**Configuration (`process_brief_enhanced.py:224`):**
|
||||
```python
|
||||
def _setup_model(self):
|
||||
return OpenAI(
|
||||
api_key=OPENAI_API_KEY,
|
||||
timeout=3600, # 60 minutes for reasoning tasks
|
||||
max_retries=2 # Reduced retries for efficiency
|
||||
)
|
||||
```
|
||||
|
||||
**API Call Pattern:**
|
||||
```python
|
||||
response = self.model.responses.parse(
|
||||
model=self.model_name, # 'gpt-5'
|
||||
input=[
|
||||
{"role": "system", "content": system_message},
|
||||
{"role": "user", "content": combined_prompt}
|
||||
],
|
||||
reasoning={"effort": self.reasoning_effort}, # high/medium/low/minimal
|
||||
text_format=AssetExtractionResult # Pydantic schema
|
||||
)
|
||||
```
|
||||
|
||||
### LlamaCloud Integration
|
||||
|
||||
**Document Processing:**
|
||||
```python
|
||||
def _extract_document_content(self, filepath: str) -> str:
|
||||
parser = LlamaParse(
|
||||
api_key=LLAMACLOUD_API_KEY,
|
||||
parse_mode="parse_page_with_agent",
|
||||
model="openai-gpt-5",
|
||||
# ... additional configuration
|
||||
)
|
||||
|
||||
result = parser.parse(filepath)
|
||||
markdown_documents = result.get_markdown_documents(split_by_page=True)
|
||||
combined_content = "\n\n".join([doc.text for doc in markdown_documents])
|
||||
|
||||
return combined_content
|
||||
```
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### Environment Setup
|
||||
|
||||
**Required Dependencies:**
|
||||
```python
|
||||
# Core AI/ML libraries
|
||||
openai>=1.0.0
|
||||
llama-cloud-services>=0.6.62
|
||||
google-generativeai>=0.3.0
|
||||
json5>=0.9.0
|
||||
|
||||
# Document processing
|
||||
python-pptx>=0.6.21
|
||||
PyMuPDF>=1.23.0
|
||||
python-docx>=0.8.11
|
||||
openpyxl>=3.1.0
|
||||
|
||||
# Data processing
|
||||
pandas>=2.0.0
|
||||
numpy>=1.24.0
|
||||
```
|
||||
|
||||
**API Keys Configuration:**
|
||||
```python
|
||||
# Required API Keys in process_brief_enhanced.py
|
||||
OPENAI_API_KEY = "your-openai-api-key" # Line 30
|
||||
LLAMACLOUD_API_KEY = "your-llama-api-key" # Line 31
|
||||
GEMINI_API_KEY = "legacy-key" # Line 29 (unused)
|
||||
```
|
||||
|
||||
### Virtual Environment Setup
|
||||
|
||||
```bash
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# Activate environment
|
||||
source venv/bin/activate # Linux/Mac
|
||||
venv\Scripts\activate # Windows
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements_enhanced.txt
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Default processing (medium reasoning)
|
||||
python process_brief_enhanced.py document.pptx
|
||||
|
||||
# High reasoning effort for complex documents
|
||||
python process_brief_enhanced.py complex_brief.pdf high
|
||||
|
||||
# Low reasoning effort for simple documents
|
||||
python process_brief_enhanced.py simple_brief.docx low
|
||||
```
|
||||
|
||||
### Command Line Arguments
|
||||
|
||||
1. **filepath** (required): Path to document file
|
||||
2. **reasoning_effort** (optional): `high`, `medium` (default), `low`, `minimal`
|
||||
|
||||
### Output Structure
|
||||
|
||||
**Console Output:**
|
||||
```
|
||||
=== ENHANCED BRIEF PROCESSING STARTED ===
|
||||
Document Type: powerpoint
|
||||
Assets Extracted: 245
|
||||
Confidence Score: 0.95
|
||||
Processing Notes: Multi-perspective analysis completed, Added 12 assets from validation
|
||||
Output File: output/document-20250102140530.csv
|
||||
|
||||
=== COST ANALYSIS ===
|
||||
Model Used: gpt-5
|
||||
Input Tokens: 45,230
|
||||
Output Tokens: 12,450
|
||||
Total Cost: $0.2376
|
||||
```
|
||||
|
||||
**CSV Output Format:**
|
||||
```csv
|
||||
title,status,category,media,asset_type,brand_identifier,format,review_date,...
|
||||
"Display Banner - Hero","Active","Display","Image","PNG","Adidas","1920x1080","2025-01-15",...
|
||||
"Social Media Post","Pending","Social","Image","JPG","Adidas","1080x1080","2025-01-10",...
|
||||
```
|
||||
|
||||
## Cost Management & Performance
|
||||
|
||||
### Token Usage Tracking
|
||||
|
||||
**Cost Calculation (`process_brief_enhanced.py:176`):**
|
||||
```python
|
||||
def calculate_cost(self, model_name: str) -> float:
|
||||
pricing = OPENAI_PRICING[model_name]
|
||||
|
||||
input_cost = (self.input_tokens / 1_000_000) * pricing['input']
|
||||
cached_cost = (self.cached_input_tokens / 1_000_000) * pricing['cached_input']
|
||||
output_cost = (self.output_tokens / 1_000_000) * pricing['output']
|
||||
|
||||
return input_cost + cached_cost + output_cost
|
||||
```
|
||||
|
||||
**GPT-5 Pricing (per 1M tokens):**
|
||||
- Input: $2.50
|
||||
- Cached Input: $1.25
|
||||
- Output: $10.00
|
||||
|
||||
### Performance Characteristics
|
||||
|
||||
**Processing Time:**
|
||||
- Simple documents (1-10 pages): 30 seconds - 2 minutes
|
||||
- Complex documents (10+ pages): 2-5 minutes
|
||||
- Reasoning effort impact: High effort adds 50-100% processing time
|
||||
|
||||
**Memory Usage:**
|
||||
- Base memory: ~200MB
|
||||
- Document processing: +50-200MB depending on document size
|
||||
- AI processing: +100-300MB during API calls
|
||||
|
||||
**Typical Token Usage:**
|
||||
- Small brief (5 pages): 10K-20K input, 2K-5K output
|
||||
- Medium brief (15 pages): 30K-50K input, 5K-10K output
|
||||
- Large brief (30+ pages): 60K-100K input, 10K-20K output
|
||||
|
||||
## Error Handling & Logging
|
||||
|
||||
### Logging Configuration (`process_brief_enhanced.py:548`)
|
||||
|
||||
```python
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('processing.log', mode='w'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Error Handling Patterns
|
||||
|
||||
**API Error Handling:**
|
||||
```python
|
||||
try:
|
||||
response = self.model.responses.parse(...)
|
||||
# Process successful response
|
||||
except Exception as e:
|
||||
logging.error(f"Multi-perspective analysis failed: {e}")
|
||||
return ProcessingResult([], {}, 0.0, [f"Analysis failed: {e}"], TokenUsage())
|
||||
```
|
||||
|
||||
**File Processing Errors:**
|
||||
```python
|
||||
try:
|
||||
document_content = self._extract_document_content(filepath)
|
||||
except Exception as e:
|
||||
logging.error(f"Content extraction failed: {e}")
|
||||
return ProcessingResult([], {}, 0.0, [f"Content extraction failed: {e}"], TokenUsage())
|
||||
```
|
||||
|
||||
### Common Error Scenarios
|
||||
|
||||
1. **API Key Issues**: Missing or invalid OpenAI/LlamaCloud API keys
|
||||
2. **File Access**: Permissions or file corruption issues
|
||||
3. **API Timeouts**: Large documents exceeding timeout limits
|
||||
4. **Rate Limits**: API rate limiting from OpenAI or LlamaCloud
|
||||
5. **Memory Issues**: Very large documents causing memory exhaustion
|
||||
|
||||
## System Comparison & Benchmarking
|
||||
|
||||
### Comparison Script (`compare_systems.py`)
|
||||
|
||||
Benchmarks the enhanced system against baseline implementations:
|
||||
|
||||
**Key Metrics:**
|
||||
- Asset extraction count
|
||||
- Processing time
|
||||
- Cost per document
|
||||
- Accuracy assessment
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python compare_systems.py
|
||||
```
|
||||
|
||||
**Expected Improvements:**
|
||||
- 50-300% increase in asset extraction completeness
|
||||
- Superior technical specification accuracy
|
||||
- Better handling of multi-language/multi-market requirements
|
||||
|
||||
## Extension Points
|
||||
|
||||
### 1. Adding New Document Types
|
||||
|
||||
**Extend DocumentType enum:**
|
||||
```python
|
||||
class DocumentType(Enum):
|
||||
POWERPOINT = "powerpoint"
|
||||
WORD = "word"
|
||||
PDF = "pdf"
|
||||
EXCEL = "excel"
|
||||
# Add new types here
|
||||
GOOGLE_SLIDES = "google_slides"
|
||||
UNKNOWN = "unknown"
|
||||
```
|
||||
|
||||
**Implement classification logic:**
|
||||
```python
|
||||
def classify_document(self, filepath: str) -> DocumentType:
|
||||
extension = os.path.splitext(filepath)[1].lower()
|
||||
# Add new extension handling
|
||||
```
|
||||
|
||||
### 2. Custom Output Formats
|
||||
|
||||
**Extend output generation:**
|
||||
```python
|
||||
def generate_json_output(self, results: ProcessingResult) -> str:
|
||||
"""Generate JSON output format"""
|
||||
assets_dict = [asset for asset in results.raw_data]
|
||||
return json.dumps(assets_dict, indent=2)
|
||||
```
|
||||
|
||||
### 3. Additional AI Models
|
||||
|
||||
**Support multiple AI providers:**
|
||||
```python
|
||||
def _setup_model(self):
|
||||
if self.model_name.startswith('gpt-'):
|
||||
return self._setup_openai()
|
||||
elif self.model_name.startswith('claude-'):
|
||||
return self._setup_anthropic()
|
||||
# Add other providers
|
||||
```
|
||||
|
||||
### 4. Custom Validation Rules
|
||||
|
||||
**Extend validation logic:**
|
||||
```python
|
||||
def _custom_validation_rules(self, assets: List[Dict]) -> List[str]:
|
||||
"""Apply business-specific validation rules"""
|
||||
issues = []
|
||||
for asset in assets:
|
||||
# Custom validation logic
|
||||
if not asset.get('format') and asset.get('media') == 'Image':
|
||||
issues.append(f"Image asset '{asset['title']}' missing format specification")
|
||||
return issues
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### API Key Management
|
||||
- Store API keys in environment variables
|
||||
- Use secure key rotation practices
|
||||
- Implement key validation before processing
|
||||
- Consider using secret management services
|
||||
|
||||
### Document Security
|
||||
- Validate file types before processing
|
||||
- Implement file size limits
|
||||
- Scan for malicious content
|
||||
- Ensure secure temporary file handling
|
||||
|
||||
### Data Privacy
|
||||
- Log sanitization (avoid logging sensitive content)
|
||||
- Secure deletion of temporary files
|
||||
- Consider data residency requirements for API calls
|
||||
- Implement audit trails for document processing
|
||||
|
||||
## Troubleshooting Guide
|
||||
|
||||
### Common Issues
|
||||
|
||||
**1. "OPENAI_API_KEY not set" Error**
|
||||
```bash
|
||||
# Solution: Set API key in process_brief_enhanced.py line 30
|
||||
OPENAI_API_KEY = "your-actual-api-key"
|
||||
```
|
||||
|
||||
**2. LlamaParser Import Error**
|
||||
```bash
|
||||
# Solution: Install llama-cloud-services
|
||||
pip install llama-cloud-services>=0.6.62
|
||||
```
|
||||
|
||||
**3. Timeout Errors on Large Documents**
|
||||
```bash
|
||||
# Solution: Use lower reasoning effort
|
||||
python process_brief_enhanced.py large_doc.pdf low
|
||||
```
|
||||
|
||||
**4. High Processing Costs**
|
||||
```bash
|
||||
# Solution: Use minimal reasoning for simple documents
|
||||
python process_brief_enhanced.py simple_doc.docx minimal
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
**1. Reduce Processing Time:**
|
||||
- Use `low` or `minimal` reasoning effort for simple documents
|
||||
- Implement document pre-filtering
|
||||
- Consider document splitting for very large files
|
||||
|
||||
**2. Reduce Costs:**
|
||||
- Monitor token usage with cost summaries
|
||||
- Use cached inputs when possible
|
||||
- Optimize prompt length and complexity
|
||||
|
||||
**3. Improve Accuracy:**
|
||||
- Use `high` reasoning effort for complex documents
|
||||
- Customize prompts for specific document types
|
||||
- Implement domain-specific validation rules
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
1. **Batch Processing**: Process multiple documents simultaneously
|
||||
2. **Document Templates**: Predefined extraction rules for common brief types
|
||||
3. **Quality Scoring**: Automated confidence scoring for extractions
|
||||
4. **Export Formats**: Support for JSON, XML, and database outputs
|
||||
5. **Integration APIs**: REST API for system integration
|
||||
6. **Real-time Monitoring**: Processing metrics and alerting
|
||||
|
||||
### Technical Roadmap
|
||||
1. **Multi-threading**: Parallel processing for large document batches
|
||||
2. **Caching Layer**: Redis-based result caching
|
||||
3. **Database Integration**: Direct database output options
|
||||
4. **Containerization**: Docker deployment support
|
||||
5. **Cloud Deployment**: AWS/GCP/Azure deployment options
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Adidas Brief Extraction System represents a sophisticated approach to automated document analysis, combining state-of-the-art AI models with robust engineering practices. The system's modular architecture, comprehensive error handling, and detailed monitoring make it suitable for production deployment in enterprise environments.
|
||||
|
||||
The multi-pass analysis pipeline ensures high accuracy in asset extraction, while the cost tracking and performance optimization features make it economically viable for large-scale document processing workflows.
|
||||
|
||||
For questions or support, refer to the project's logging output and error handling mechanisms, which provide detailed debugging information for troubleshooting and optimization.
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
# Adidas Brief Extractor - System Flow Chart
|
||||
|
||||
This flowchart shows how the Adidas Brief Extraction system processes documents to extract marketing asset information.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Input Layer"
|
||||
A[Document Files] --> B{Document Classifier}
|
||||
B --> C[PowerPoint]
|
||||
B --> D[Word]
|
||||
B --> E[PDF]
|
||||
B --> F[Excel]
|
||||
end
|
||||
|
||||
subgraph "Preprocessing Layer"
|
||||
C --> G[LlamaParser Cloud]
|
||||
D --> G
|
||||
E --> G
|
||||
F --> G
|
||||
G --> H[Structured Markdown]
|
||||
end
|
||||
|
||||
subgraph "AI Processing Layer"
|
||||
H --> I[DocumentAnalyzer]
|
||||
I --> J[Multi-Perspective Analysis]
|
||||
J --> K[Cross-Validation & Enhancement]
|
||||
K --> L[Asset Extraction Result]
|
||||
end
|
||||
|
||||
subgraph "Output Layer"
|
||||
L --> M[CSV Generator]
|
||||
M --> N[Structured CSV Output]
|
||||
end
|
||||
|
||||
subgraph "External Services"
|
||||
O[OpenAI GPT-5 API] --> J
|
||||
O --> K
|
||||
P[LlamaCloud API] --> G
|
||||
end
|
||||
|
||||
subgraph "Monitoring & Logging"
|
||||
Q[Token Usage Tracker] --> I
|
||||
R[Cost Calculator] --> Q
|
||||
S[Processing Logger] --> I
|
||||
end
|
||||
```
|
||||
|
||||
## Section Explanations
|
||||
|
||||
### Input Layer
|
||||
**What it does:** The system accepts various types of business documents including PowerPoint presentations, Word documents, PDFs, and Excel spreadsheets, then identifies what type of document it's working with.
|
||||
|
||||
### Preprocessing Layer
|
||||
**What it does:** In this section, the tool converts all document types into a clean, standardized text format that can be easily read and analyzed by removing formatting complexity and organizing the content properly.
|
||||
|
||||
### AI Processing Layer
|
||||
**What it does:** This is where the artificial intelligence analyzes the cleaned document content from multiple professional viewpoints (like a project manager, creative director, and technical specialist) to identify and extract all marketing deliverables, then double-checks the results for completeness and accuracy.
|
||||
|
||||
### Output Layer
|
||||
**What it does:** The system takes all the identified marketing assets and organizes them into a structured spreadsheet format with standardized columns for easy review, tracking, and project management.
|
||||
|
||||
### External Services
|
||||
**What it does:** The system connects to cloud-based AI services - one for document processing (LlamaCloud) and another for intelligent analysis (OpenAI's GPT-5) - to leverage advanced capabilities that would be impossible to run locally.
|
||||
|
||||
### Monitoring & Logging
|
||||
**What it does:** This tracks how much the AI services cost to use, monitors system performance, and keeps detailed logs of what happened during processing for troubleshooting and cost management purposes.
|
||||
|
||||
---
|
||||
|
||||
## Overall Process Summary
|
||||
|
||||
1. **Upload** → You provide a business document (brief, presentation, etc.)
|
||||
2. **Clean** → The system converts it to readable text format
|
||||
3. **Analyze** → AI examines the content to find all marketing deliverables
|
||||
4. **Validate** → Double-checks to ensure nothing was missed
|
||||
5. **Export** → Creates a structured spreadsheet with all identified assets
|
||||
|
||||
The entire process typically takes 30 seconds to 5 minutes depending on document complexity, and provides detailed cost tracking so you know exactly what each analysis costs to run.
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -1,593 +0,0 @@
|
|||
# Enhanced Brief Processing System v2.0 - Technical Architecture
|
||||
|
||||
> **Evolution of Document Intelligence: From Monolithic to Symphonic**
|
||||
> A sophisticated multi-model AI platform for marketing asset extraction
|
||||
|
||||
## System Genesis & Architectural Philosophy
|
||||
|
||||
The Enhanced Brief Processing System represents a paradigm shift in document analysis architecture. What began as a straightforward single-model extraction tool has evolved into a distributed AI consensus system that leverages multiple state-of-the-art language models to achieve unprecedented accuracy in marketing asset identification and specification extraction.
|
||||
|
||||
The fundamental insight driving this evolution: no single AI model, regardless of sophistication, captures the complete complexity of marketing brief documentation. By orchestrating multiple models in parallel and synthesizing their outputs through intelligent consolidation, we achieve a level of comprehensiveness and reliability that exceeds any individual model's capabilities.
|
||||
|
||||
## Architectural Evolution
|
||||
|
||||
### Phase I: Monolithic Simplicity
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Document] --> B[LlamaParser]
|
||||
B --> C[GPT-5 Analysis]
|
||||
C --> D[CSV Export]
|
||||
|
||||
style C fill:#ff6b6b
|
||||
```
|
||||
|
||||
**Limitations:** Single point of failure, provider lock-in, limited perspective diversity
|
||||
|
||||
### Phase II: Multi-Model Orchestration
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Document] --> B[LlamaParser Enhanced]
|
||||
B --> C[Provider Manager]
|
||||
|
||||
C --> D1[GPT-5<br/>Reasoning Engine]
|
||||
C --> D2[Claude Sonnet<br/>Analysis Specialist]
|
||||
C --> D3[Gemini Pro<br/>Context Virtuoso]
|
||||
|
||||
D1 --> E[Consolidation Intelligence]
|
||||
D2 --> E
|
||||
D3 --> E
|
||||
|
||||
E --> F[Multiplier Expansion]
|
||||
F --> G[Validated Output]
|
||||
|
||||
style C fill:#4ecdc4
|
||||
style E fill:#45b7d1
|
||||
style F fill:#f9ca24
|
||||
```
|
||||
|
||||
**Advantages:** Fault tolerance, perspective diversity, performance optimization, provider flexibility
|
||||
|
||||
## Multi-Provider Architecture
|
||||
|
||||
### Provider Abstraction Framework
|
||||
|
||||
The `llm_service` layer implements a sophisticated adapter pattern that normalizes the inherent chaos of multiple AI providers into a coherent, unified interface:
|
||||
|
||||
```python
|
||||
class BaseLLMProvider(ABC):
|
||||
@abstractmethod
|
||||
async def generate_response(self, messages, schema=None) -> LLMResponse
|
||||
```
|
||||
|
||||
**Provider Specializations:**
|
||||
|
||||
**OpenAI Provider** - Leverages GPT-5's reasoning effort capabilities with structured output through the responses API. The implementation exploits OpenAI's native `oneOf` schema support and cached token optimization.
|
||||
|
||||
**Anthropic Provider** - Utilizes Claude's tool-based structured output system with sophisticated message format adaptation. The provider intelligently selects between Opus (maximum quality) and Sonnet (balanced performance) variants.
|
||||
|
||||
**Google Provider** - Integrates Gemini 2.5 Pro through advanced schema translation that converts OpenAI-style JSON schemas to Google's native format, handling the massive 2M token context window effectively.
|
||||
|
||||
### Parallel Execution Engine
|
||||
|
||||
The provider manager orchestrates true concurrent processing through sophisticated async task management:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant PM as Provider Manager
|
||||
participant O as OpenAI
|
||||
participant A as Anthropic
|
||||
participant G as Google
|
||||
|
||||
PM->>PM: create_parallel_tasks()
|
||||
|
||||
par Simultaneous Analysis
|
||||
PM->>O: analyze_async()
|
||||
PM->>A: analyze_async()
|
||||
PM->>G: analyze_async()
|
||||
end
|
||||
|
||||
O-->>PM: BaseDeliverables
|
||||
A-->>PM: BaseDeliverables
|
||||
G-->>PM: BaseDeliverables
|
||||
|
||||
PM->>PM: consolidate_results()
|
||||
```
|
||||
|
||||
**Performance Transformation:**
|
||||
- **Sequential Processing**: Σ(model_times) = cumulative delay
|
||||
- **Parallel Processing**: max(model_times) = optimal efficiency
|
||||
|
||||
## Universal Schema System
|
||||
|
||||
### Cross-Provider Compatibility Revolution
|
||||
|
||||
The universal schema represents a breakthrough in AI provider interoperability. Rather than maintaining separate schemas or complex conversion logic, we developed a mixed-type schema that leverages each provider's strengths:
|
||||
|
||||
```json
|
||||
{
|
||||
"technical_specifications": {
|
||||
"type": "array",
|
||||
"description": "MULTIPLIER FIELD: Dimensions and requirements"
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Asset category (e.g., 'Social Media')"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Design Philosophy:**
|
||||
- **Multiplier Fields** (arrays): Only fields that legitimately vary across asset instances
|
||||
- **Metadata Fields** (strings): Fixed properties that describe the asset type
|
||||
- **Validation Fields** (strings): Quantity targets for mathematical verification
|
||||
|
||||
### Multiplier Mathematics
|
||||
|
||||
The system implements precise combinatorial logic for asset expansion:
|
||||
|
||||
**Before:** Exponential chaos through indiscriminate field multiplication
|
||||
**After:** Controlled expansion through mathematical rigor
|
||||
|
||||
```python
|
||||
# Only meaningful multipliers participate in expansion
|
||||
multiplier_field_names = {'technical_specifications', 'language_country_market'}
|
||||
|
||||
# Cartesian product with validation
|
||||
combinations = itertools.product(*[multiplier_fields[field] for field in field_names])
|
||||
actual_count = len(list(combinations))
|
||||
|
||||
# Mathematical verification against expected quantity
|
||||
if expected_quantity and actual_count != expected_quantity:
|
||||
generate_quantity_mismatch_warning()
|
||||
```
|
||||
|
||||
## Consolidation Intelligence
|
||||
|
||||
### Multi-Model Synthesis Engine
|
||||
|
||||
The consolidation system employs sophisticated normalization and deduplication algorithms that transcend simple voting or averaging mechanisms:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Model Results] --> B[Normalization Engine]
|
||||
B --> C[Title Canonicalization]
|
||||
B --> D[Category Harmonization]
|
||||
B --> E[Field Standardization]
|
||||
|
||||
C --> F[Deduplication Matrix]
|
||||
D --> F
|
||||
E --> F
|
||||
|
||||
F --> G[Inclusion Logic<br/>"Any Model Found It"]
|
||||
G --> H[Quality Enhancement<br/>Best Specs from All]
|
||||
H --> I[Validated Output]
|
||||
|
||||
style F fill:#dda0dd
|
||||
style G fill:#98fb98
|
||||
```
|
||||
|
||||
**Consolidation Philosophy:**
|
||||
- **Inclusive Bias**: Err on the side of completeness rather than conservative exclusion
|
||||
- **Intelligent Deduplication**: Distinguish genuine duplicates from legitimate variations
|
||||
- **Quality Synthesis**: Combine the strongest elements from each model's analysis
|
||||
- **Validation Integration**: Ensure mathematical consistency in final output
|
||||
|
||||
### Advanced Deduplication Logic
|
||||
|
||||
The system implements multi-dimensional similarity analysis:
|
||||
|
||||
```text
|
||||
Deduplication Key = f(normalized_title, category, media, technical_specs, asset_type)
|
||||
|
||||
Merge Conditions:
|
||||
- Identical core identity with overlapping specifications
|
||||
- Title variations that represent the same underlying deliverable
|
||||
- Complementary multiplier arrays that can be unified
|
||||
|
||||
Separation Conditions:
|
||||
- Distinct technical requirements (different dimensions, formats)
|
||||
- Different media types or asset categories
|
||||
- Non-overlapping market/language requirements
|
||||
```
|
||||
|
||||
## Async Architecture Excellence
|
||||
|
||||
### Concurrent Processing Implementation
|
||||
|
||||
The system achieves true parallelism through sophisticated async orchestration:
|
||||
|
||||
**Provider Level:**
|
||||
- **AsyncOpenAI**: Native async client with reasoning effort control
|
||||
- **AsyncAnthropic**: Tool-based structured output with async message creation
|
||||
- **Google GenAI**: `.aio` interface for non-blocking generation
|
||||
|
||||
**System Level:**
|
||||
```python
|
||||
# Elegant parallel execution with fault tolerance
|
||||
task_results = await asyncio.gather(*[task for _, task in tasks], return_exceptions=True)
|
||||
|
||||
# Intelligent result processing
|
||||
for i, result in enumerate(task_results):
|
||||
if isinstance(result, Exception):
|
||||
handle_provider_failure(model_keys[i], result)
|
||||
else:
|
||||
process_successful_response(result)
|
||||
```
|
||||
|
||||
## Cost Intelligence & Optimization
|
||||
|
||||
### Multi-Provider Economic Model
|
||||
|
||||
The system implements sophisticated cost tracking and optimization across providers with vastly different pricing structures:
|
||||
|
||||
| Provider | Model | Context | Input/1M | Output/1M | Strategic Use |
|
||||
|----------|-------|---------|----------|-----------|---------------|
|
||||
| OpenAI | GPT-5 | 200k | $2.50 | $10.00 | Complex reasoning |
|
||||
| Anthropic | Opus 4.1 | 200k | $15.00 | $75.00 | Maximum quality |
|
||||
| Anthropic | Sonnet 4 | 200k | $3.00 | $15.00 | Balanced performance |
|
||||
| Google | Gemini 2.5 Pro | 2M | $1.25 | $5.00 | Cost optimization |
|
||||
|
||||
**Cost Optimization Strategies:**
|
||||
- **Pre-processing estimation** with user confirmation thresholds
|
||||
- **Real-time tracking** across all concurrent model executions
|
||||
- **Provider-specific optimizations** (cached tokens, reasoning effort, context management)
|
||||
- **Budget controls** with configurable spending limits
|
||||
|
||||
## Document Processing Pipeline
|
||||
|
||||
### Enhanced LlamaParser Integration
|
||||
|
||||
The document preprocessing layer demonstrates sophisticated parsing optimization:
|
||||
|
||||
```python
|
||||
parser = LlamaParse(
|
||||
parse_mode="parse_page_with_agent", # AI-powered structure understanding
|
||||
model="openai-gpt-5", # Best available parsing model
|
||||
high_res_ocr=True, # Maximum text recognition accuracy
|
||||
adaptive_long_table=True, # Complex table structure handling
|
||||
output_tables_as_HTML=True # Preserved formatting for LLM analysis
|
||||
)
|
||||
```
|
||||
|
||||
**Multi-Format Excellence:**
|
||||
- **PowerPoint**: Slide-by-slide extraction with preserved hierarchy
|
||||
- **Word**: Paragraph and table content with formatting retention
|
||||
- **PDF**: Page-by-page analysis with high-resolution OCR
|
||||
- **Excel**: Multi-sheet data extraction with cell relationship preservation
|
||||
|
||||
## Prompt Engineering Sophistication
|
||||
|
||||
### Multi-Perspective Analysis Framework
|
||||
|
||||
The prompt system evolved from basic instructions to sophisticated AI guidance frameworks that encode domain expertise:
|
||||
|
||||
**Multiplier Detection Intelligence:**
|
||||
```text
|
||||
**What counts as a multiplier (make arrays):**
|
||||
- Technical Specifications: dimensions, durations, versions
|
||||
- Language-Country-Market Combinations: ISO format semantic pairs
|
||||
- Location/Market Variations: when adaptation required for different markets
|
||||
|
||||
**What is NOT a multiplier (treat as metadata):**
|
||||
- Top-level taxonomy labels used as constant headers
|
||||
- Campaign/Project/Initiative names that don't vary
|
||||
- Status, category, media type (unless explicitly multi-variant)
|
||||
```
|
||||
|
||||
### Consolidation Strategy Framework
|
||||
|
||||
The consolidation prompt implements diplomatic negotiation principles for AI model consensus:
|
||||
|
||||
**Normalization Before Deduplication:**
|
||||
- Title canonicalization removes multipliers for consistent comparison
|
||||
- Category harmonization merges similar taxonomies across models
|
||||
- Field standardization ensures semantic consistency
|
||||
|
||||
**Intelligent Merging Logic:**
|
||||
- Union multiplier arrays while preserving uniqueness
|
||||
- Select highest quality specifications from any contributing model
|
||||
- Maintain validation relationships between fields
|
||||
|
||||
## Error Handling & Resilience
|
||||
|
||||
### Multi-Layer Fault Tolerance
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[API Request] --> B{Provider Available?}
|
||||
B -->|No| C[Mark Failed + Continue]
|
||||
B -->|Yes| D[Execute Request]
|
||||
|
||||
D --> E{Response Valid?}
|
||||
E -->|No| F[Log Error + Fallback]
|
||||
E -->|Yes| G[Parse Response]
|
||||
|
||||
G --> H{JSON Valid?}
|
||||
H -->|No| I[Alternative Parsing]
|
||||
H -->|Yes| J[Success]
|
||||
|
||||
C --> K{Min Threshold Met?}
|
||||
F --> K
|
||||
I --> K
|
||||
J --> K
|
||||
|
||||
K -->|Yes| L[Continue Pipeline]
|
||||
K -->|No| M[Abort with Diagnostics]
|
||||
|
||||
style C fill:#ffa726
|
||||
style F fill:#ffa726
|
||||
style L fill:#66bb6a
|
||||
style M fill:#ef5350
|
||||
```
|
||||
|
||||
**Resilience Principles:**
|
||||
- **Graceful Degradation**: Continue with successful models when others fail
|
||||
- **Comprehensive Diagnostics**: Detailed error context for troubleshooting
|
||||
- **Configurable Thresholds**: Flexible minimum success requirements
|
||||
- **Exception Isolation**: Provider failures don't cascade to system failure
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Processing Optimization Analysis
|
||||
|
||||
**Sequential vs Parallel Performance:**
|
||||
|
||||
| Document Size | Sequential | Parallel | Improvement |
|
||||
|---------------|------------|----------|-------------|
|
||||
| Small (1-5 pages) | 120s | 75s | 38% faster |
|
||||
| Medium (6-20 pages) | 210s | 95s | 55% faster |
|
||||
| Large (20+ pages) | 340s | 140s | 59% faster |
|
||||
|
||||
**Memory Efficiency:**
|
||||
- **Streaming expansion** prevents memory overflow during large asset generation
|
||||
- **Token usage optimization** through provider-specific caching strategies
|
||||
- **Garbage collection** awareness in async task management
|
||||
|
||||
## Quality Assurance Framework
|
||||
|
||||
### Validation & Verification Systems
|
||||
|
||||
**Expansion Validation:**
|
||||
```python
|
||||
# Mathematical verification of multiplier expansion
|
||||
expected_quantity = int(base_deliverable.quantity)
|
||||
actual_expansion = len(technical_specs) * len(markets)
|
||||
|
||||
if abs(expected_quantity - actual_expansion) > tolerance:
|
||||
generate_expansion_warning()
|
||||
```
|
||||
|
||||
**Consolidation Quality Metrics:**
|
||||
- **Coverage Analysis**: Ensure no model's unique findings are lost
|
||||
- **Consistency Scoring**: Measure agreement levels across models
|
||||
- **Completeness Verification**: Validate against original document structure
|
||||
|
||||
## Configuration Management Excellence
|
||||
|
||||
### Environment-Driven Architecture
|
||||
|
||||
The configuration system demonstrates sophisticated separation of concerns:
|
||||
|
||||
```python
|
||||
class Config:
|
||||
# Provider-specific configuration with validation
|
||||
@classmethod
|
||||
def get_provider_config(cls, provider: str) -> Dict[str, Any]:
|
||||
# Dynamic configuration retrieval with defaults
|
||||
|
||||
@classmethod
|
||||
def validate_api_keys(cls) -> Dict[str, bool]:
|
||||
# Comprehensive credential validation
|
||||
```
|
||||
|
||||
**Configuration Hierarchy:**
|
||||
1. **Environment Variables** (.env) - Secure credential and setting storage
|
||||
2. **Default Values** (config.py) - Sensible fallbacks and validation
|
||||
3. **Runtime Parameters** (CLI) - Dynamic model selection and processing options
|
||||
4. **Provider Specifics** - Model-specific optimizations and constraints
|
||||
|
||||
## Data Flow Architecture
|
||||
|
||||
### Complete Processing Journey
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Document Upload] --> B[Type Classification]
|
||||
B --> C[LlamaParser Extraction]
|
||||
C --> D[Multi-Model Analysis]
|
||||
|
||||
subgraph "Parallel Processing Cluster"
|
||||
D --> E1[GPT-5 Analysis]
|
||||
D --> E2[Claude Analysis]
|
||||
D --> E3[Gemini Analysis]
|
||||
end
|
||||
|
||||
E1 --> F[Result Aggregation]
|
||||
E2 --> F
|
||||
E3 --> F
|
||||
|
||||
F --> G[Consolidation Engine]
|
||||
G --> H[Normalized Base Deliverables]
|
||||
H --> I[Multiplier Expansion Engine]
|
||||
I --> J[Individual Asset Generation]
|
||||
J --> K[CSV Export & Validation]
|
||||
|
||||
subgraph "Quality Assurance Layer"
|
||||
G
|
||||
H
|
||||
I
|
||||
J
|
||||
end
|
||||
|
||||
style D fill:#74b9ff
|
||||
style G fill:#a29bfe
|
||||
style I fill:#ffeaa7
|
||||
```
|
||||
|
||||
## Advanced Feature Analysis
|
||||
|
||||
### Multiplier System Sophistication
|
||||
|
||||
The multiplier expansion system represents a mathematical approach to document analysis that eliminates both under-counting and over-counting through principled constraint application:
|
||||
|
||||
**Controlled Multiplication:**
|
||||
- **Technical Specifications**: Legitimate size/format variations
|
||||
- **Language-Country-Market**: Semantic ISO-coded market combinations
|
||||
- **Validation Integration**: Quantity field provides expansion verification
|
||||
|
||||
**Mathematical Precision:**
|
||||
```
|
||||
Base Deliverable: "Display Campaign"
|
||||
Specifications: ["728x90", "300x250", "160x600"] (3 formats)
|
||||
Markets: ["EN-UK", "DE-DE", "FR-FR"] (3 regions)
|
||||
Quantity Validation: "9" (3 × 3 = 9 ✓)
|
||||
```
|
||||
|
||||
### Language-Country Market Fusion
|
||||
|
||||
The elegant solution to the language-country multiplication problem:
|
||||
|
||||
**Previous Approach:**
|
||||
```
|
||||
Languages: ["EN", "DE", "FR"] × Countries: ["UK", "DE", "FR"] = 9 combinations
|
||||
Including semantically invalid pairs: "EN-DE", "DE-UK"
|
||||
```
|
||||
|
||||
**Current Approach:**
|
||||
```
|
||||
Language-Country-Market: ["EN-UK", "DE-DE", "FR-FR"] = 3 logical combinations
|
||||
Semantic validity maintained through ISO-coded market specification
|
||||
```
|
||||
|
||||
## Prompt Engineering Excellence
|
||||
|
||||
### Multi-Perspective Analysis Design
|
||||
|
||||
The prompt architecture encodes sophisticated domain knowledge about marketing asset extraction:
|
||||
|
||||
**Strategic Extraction Methodology:**
|
||||
- **Base-first approach**: Identify deliverable types before multiplier enumeration
|
||||
- **Multiplier vigilance**: Distinguish true variations from taxonomic labels
|
||||
- **Validation integration**: Quantity field provides mathematical constraint
|
||||
- **Normalization guidance**: Canonical title and category formatting
|
||||
|
||||
### Consolidation Strategy Framework
|
||||
|
||||
The consolidation prompt implements diplomatic consensus-building for AI models:
|
||||
|
||||
**Synthesis Principles:**
|
||||
- **Inclusive bias**: Preserve unique findings from any model
|
||||
- **Normalization precedence**: Standardize before comparison
|
||||
- **Quality enhancement**: Optimize specifications through multi-model synthesis
|
||||
- **Mathematical validation**: Ensure expansion consistency
|
||||
|
||||
## System Integration & Extensibility
|
||||
|
||||
### Plugin Architecture for Provider Addition
|
||||
|
||||
```python
|
||||
# Adding new providers follows standardized pattern
|
||||
class NewProviderImplementation(BaseLLMProvider):
|
||||
async def generate_response(self, messages, schema=None):
|
||||
# Provider-specific implementation
|
||||
# System automatically integrates through abstraction layer
|
||||
```
|
||||
|
||||
### Schema Evolution Framework
|
||||
|
||||
External schema management enables rapid iteration:
|
||||
- **JSON-based definition** in `prompts/universal_schema.json`
|
||||
- **Hot-swappable** without code modification
|
||||
- **Provider-agnostic** design ensures universal compatibility
|
||||
- **Version management** through external file versioning
|
||||
|
||||
## Performance Monitoring & Observability
|
||||
|
||||
### Comprehensive Telemetry
|
||||
|
||||
The system implements enterprise-grade monitoring across the processing pipeline:
|
||||
|
||||
**Model Performance Tracking:**
|
||||
```python
|
||||
# Sophisticated deliverable count analysis
|
||||
deliverable_counts = [count_deliverables(response) for response in responses]
|
||||
avg_deliverables = sum(deliverable_counts) / len(deliverable_counts)
|
||||
logging.info(f"Average deliverables across {len(deliverable_counts)} models: {avg_deliverables:.1f}")
|
||||
```
|
||||
|
||||
**Cost Intelligence:**
|
||||
- **Real-time tracking** across all concurrent model executions
|
||||
- **Provider-specific optimization** recommendations
|
||||
- **Budget alerts** with processing continuation controls
|
||||
- **Historical analysis** for cost prediction improvement
|
||||
|
||||
## Technical Innovation Highlights
|
||||
|
||||
### Async Architecture Mastery
|
||||
|
||||
The system demonstrates sophisticated understanding of Python async capabilities:
|
||||
- **Native async clients** across all providers (AsyncOpenAI, AsyncAnthropic, client.aio)
|
||||
- **Parallel task orchestration** through asyncio.gather with exception handling
|
||||
- **Resource management** with proper client lifecycle management
|
||||
- **Performance optimization** through concurrent request execution
|
||||
|
||||
### Schema Translation Intelligence
|
||||
|
||||
The Google provider's schema conversion represents elegant solution to provider incompatibility:
|
||||
- **Type mapping** from OpenAI format to Google specifications
|
||||
- **Structure preservation** while removing unsupported constructs
|
||||
- **Automatic adaptation** without manual intervention requirements
|
||||
- **Semantic equivalence** maintenance across conversion
|
||||
|
||||
### Multiplier Expansion Algorithms
|
||||
|
||||
The expansion engine implements mathematical precision in document analysis:
|
||||
- **Cartesian product generation** through itertools.product
|
||||
- **Validation integration** with quantity field verification
|
||||
- **Memory efficiency** through streaming asset generation
|
||||
- **Comprehensive logging** for expansion calculation transparency
|
||||
|
||||
## Production Readiness Features
|
||||
|
||||
### Enterprise-Grade Reliability
|
||||
|
||||
**Configuration Management:**
|
||||
- Environment-based credential storage with validation
|
||||
- Provider-specific optimization parameters
|
||||
- Flexible model selection with runtime configuration
|
||||
- Comprehensive default value management
|
||||
|
||||
**Error Handling:**
|
||||
- Multi-layer exception management with context preservation
|
||||
- Graceful degradation patterns with configurable thresholds
|
||||
- Detailed diagnostic information for troubleshooting
|
||||
- Automatic recovery mechanisms where appropriate
|
||||
|
||||
**Monitoring & Observability:**
|
||||
- Comprehensive logging across all processing stages
|
||||
- Performance metrics collection and analysis
|
||||
- Cost tracking with provider-specific breakdowns
|
||||
- Quality assurance metrics for validation
|
||||
|
||||
## Conclusion: Architectural Achievement
|
||||
|
||||
The Enhanced Brief Processing System v2.0 represents a sophisticated fusion of artificial intelligence orchestration, mathematical precision, and software engineering excellence. The transformation from single-model simplicity to multi-model sophistication demonstrates how thoughtful architecture can amplify AI capabilities while maintaining system reliability and cost efficiency.
|
||||
|
||||
**Technical Achievements:**
|
||||
- **Multi-model orchestration** with intelligent consensus building
|
||||
- **Universal schema system** enabling provider interoperability
|
||||
- **Mathematical expansion engine** with validation integration
|
||||
- **Async architecture** delivering performance optimization
|
||||
- **Enterprise-grade reliability** through comprehensive error handling
|
||||
|
||||
**Engineering Excellence:**
|
||||
- **Clean abstractions** that hide complexity while enabling flexibility
|
||||
- **Extensible design** supporting future AI model integration
|
||||
- **Sophisticated monitoring** providing operational transparency
|
||||
- **Configuration sophistication** enabling deployment flexibility
|
||||
|
||||
The system stands as a testament to the principle that well-engineered software can transform cutting-edge AI capabilities into reliable, scalable, production-ready solutions that deliver consistent business value.
|
||||
|
||||
---
|
||||
|
||||
*Architecture is the art of making complex systems appear simple to their users while maintaining sophisticated capabilities under the surface.*
|
||||
Binary file not shown.
|
|
@ -1,316 +0,0 @@
|
|||
# Enhanced Brief Processing System v2.0 - Technical Architecture
|
||||
|
||||
> **From Single-Model Constraints to Multi-Model Intelligence**
|
||||
> Sophisticated AI orchestration for marketing asset extraction
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The Enhanced Brief Processing System v2.0 transforms unstructured marketing documents into precise asset inventories through parallel multi-model analysis and intelligent consolidation. This evolution from single-model extraction to distributed AI consensus represents a paradigm shift in document analysis architecture, achieving unprecedented accuracy while maintaining cost efficiency and operational reliability.
|
||||
|
||||
**Core Innovation:** Multi-model orchestration with mathematical multiplier expansion and intelligent deduplication, processing documents through OpenAI GPT-5, Claude Opus/Sonnet, and Gemini 2.5 Pro simultaneously for comprehensive asset discovery.
|
||||
|
||||
## Architecture Evolution & Design Philosophy
|
||||
|
||||
### System Transformation
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "Phase I: Monolithic"
|
||||
A1[Document] --> B1[LlamaParser]
|
||||
B1 --> C1[Single GPT-5]
|
||||
C1 --> D1[Basic CSV]
|
||||
end
|
||||
|
||||
subgraph "Phase II: Distributed Intelligence"
|
||||
A2[Document] --> B2[Enhanced Parser]
|
||||
B2 --> C2[Provider Manager]
|
||||
|
||||
C2 --> D2[GPT-5 Reasoning]
|
||||
C2 --> E2[Claude Analysis]
|
||||
C2 --> F2[Gemini Context]
|
||||
|
||||
D2 --> G2[Consolidation Engine]
|
||||
E2 --> G2
|
||||
F2 --> G2
|
||||
|
||||
G2 --> H2[Multiplier Expansion]
|
||||
H2 --> I2[Validated Assets]
|
||||
end
|
||||
|
||||
style C1 fill:#ff6b6b
|
||||
style C2 fill:#4ecdc4
|
||||
style G2 fill:#a29bfe
|
||||
style H2 fill:#ffeaa7
|
||||
```
|
||||
|
||||
**Architectural Principles:**
|
||||
- **Provider Abstraction**: Universal interface across heterogeneous AI systems
|
||||
- **Parallel Execution**: Concurrent model processing with fault tolerance
|
||||
- **Intelligent Synthesis**: Multi-model consensus through advanced consolidation
|
||||
- **Mathematical Precision**: Controlled multiplier expansion with validation
|
||||
|
||||
### Multi-Provider Service Layer
|
||||
|
||||
The `llm_service` abstraction implements sophisticated adapter patterns that normalize provider-specific APIs into coherent interfaces:
|
||||
|
||||
```python
|
||||
class BaseLLMProvider(ABC):
|
||||
@abstractmethod
|
||||
async def generate_response(self, messages, schema=None) -> LLMResponse
|
||||
```
|
||||
|
||||
**Provider Specializations:**
|
||||
- **OpenAI**: GPT-5 reasoning effort optimization with structured response parsing
|
||||
- **Anthropic**: Tool-based output through AsyncAnthropic with model variant selection
|
||||
- **Google**: Schema translation with massive context window utilization
|
||||
|
||||
**Parallel Orchestration:**
|
||||
```python
|
||||
# Elegant concurrent execution with exception handling
|
||||
task_results = await asyncio.gather(*[task for _, task in tasks], return_exceptions=True)
|
||||
```
|
||||
|
||||
## Universal Schema & Multiplier Mathematics
|
||||
|
||||
### Schema Design Revolution
|
||||
|
||||
**Evolution from Chaos to Precision:**
|
||||
```json
|
||||
// Before: Hybrid complexity causing provider incompatibility
|
||||
{"field": {"oneOf": [{"type": "string"}, {"type": "array"}]}}
|
||||
|
||||
// After: Universal compatibility with intelligent field typing
|
||||
{
|
||||
"technical_specifications": {"type": "array", "description": "MULTIPLIER FIELD"},
|
||||
"category": {"type": "string", "description": "Asset classification"}
|
||||
}
|
||||
```
|
||||
|
||||
**Strategic Field Classification:**
|
||||
- **Multiplier Fields** (arrays): `technical_specifications`, `language_country_market`
|
||||
- **Metadata Fields** (strings): All other descriptive properties
|
||||
- **Validation Fields**: `quantity` for mathematical verification
|
||||
|
||||
### Mathematical Expansion Engine
|
||||
|
||||
**Controlled Combinatorial Logic:**
|
||||
```python
|
||||
# Precise multiplier identification and expansion
|
||||
multiplier_field_names = {'technical_specifications', 'language_country_market'}
|
||||
combinations = itertools.product(*[multiplier_fields[field] for field in field_names])
|
||||
|
||||
# Validation against expected quantity
|
||||
if actual_count != expected_quantity:
|
||||
generate_expansion_warning()
|
||||
```
|
||||
|
||||
**Transformation Impact:**
|
||||
- **Before**: Exponential explosion through indiscriminate field multiplication
|
||||
- **After**: Mathematical precision with only 2 multiplier fields
|
||||
- **Result**: Deliverable counts that align with business reality
|
||||
|
||||
## Consolidation Intelligence & Quality Synthesis
|
||||
|
||||
### Multi-Model Consensus Engine
|
||||
|
||||
The consolidation system implements sophisticated diplomatic negotiation for AI model outputs:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Model Outputs] --> B[Normalization Engine]
|
||||
B --> C[Deduplication Matrix]
|
||||
C --> D[Quality Enhancement]
|
||||
D --> E[Validation Layer]
|
||||
|
||||
subgraph "Normalization"
|
||||
B1[Title Canonicalization]
|
||||
B2[Category Harmonization]
|
||||
B3[Field Standardization]
|
||||
end
|
||||
|
||||
subgraph "Intelligence"
|
||||
C1[Similarity Analysis]
|
||||
C2[Merge Decisions]
|
||||
C3[Uniqueness Preservation]
|
||||
end
|
||||
|
||||
B --> B1
|
||||
B --> B2
|
||||
B --> B3
|
||||
|
||||
C --> C1
|
||||
C --> C2
|
||||
C --> C3
|
||||
|
||||
style B fill:#dda0dd
|
||||
style C fill:#98fb98
|
||||
style D fill:#87ceeb
|
||||
```
|
||||
|
||||
**Consolidation Philosophy:**
|
||||
- **Inclusive Bias**: "If any model found it, include it" - favor completeness over conservative exclusion
|
||||
- **Intelligent Deduplication**: Multi-dimensional similarity analysis distinguishing duplicates from legitimate variations
|
||||
- **Quality Synthesis**: Combine optimal specifications from all contributing models
|
||||
- **Mathematical Validation**: Ensure expansion consistency through quantity verification
|
||||
|
||||
### Advanced Deduplication Logic
|
||||
|
||||
**Deduplication Key Generation:**
|
||||
```
|
||||
normalized_title + category + media + technical_specifications + asset_type
|
||||
```
|
||||
|
||||
**Merge Conditions**: Identical core identity with complementary multiplier arrays
|
||||
**Separation Conditions**: Distinct technical requirements or non-overlapping specifications
|
||||
|
||||
## Performance & Cost Intelligence
|
||||
|
||||
### Concurrent Processing Optimization
|
||||
|
||||
**Performance Characteristics:**
|
||||
|
||||
| Document Type | Sequential | Parallel | Efficiency Gain |
|
||||
|---------------|------------|----------|-----------------|
|
||||
| Complex Brief | 240s | 95s | 60% improvement |
|
||||
| Standard Document | 150s | 70s | 53% improvement |
|
||||
| Simple Brief | 90s | 50s | 44% improvement |
|
||||
|
||||
### Multi-Provider Economic Model
|
||||
|
||||
**Strategic Cost Management:**
|
||||
- **Pre-processing estimation** with configurable budget limits
|
||||
- **Real-time tracking** across concurrent model executions
|
||||
- **Provider optimization** based on quality/cost analysis
|
||||
- **Dynamic model selection** supporting cost-conscious processing
|
||||
|
||||
**Provider Economics:**
|
||||
- **OpenAI GPT-5**: Premium reasoning capabilities ($2.50-$10.00/1M)
|
||||
- **Claude Opus 4.1**: Maximum quality analysis ($15.00-$75.00/1M)
|
||||
- **Claude Sonnet 4**: Balanced performance ($3.00-$15.00/1M)
|
||||
- **Gemini 2.5 Pro**: Cost-effective processing ($1.25-$5.00/1M)
|
||||
|
||||
## Error Handling & System Resilience
|
||||
|
||||
### Fault Tolerance Architecture
|
||||
|
||||
**Multi-Layer Protection:**
|
||||
```python
|
||||
# Provider-level resilience with graceful degradation
|
||||
try:
|
||||
responses = await execute_parallel_analysis()
|
||||
successful_models = [r for r in responses if r.success]
|
||||
|
||||
if len(successful_models) >= minimum_threshold:
|
||||
proceed_with_consolidation()
|
||||
else:
|
||||
implement_fallback_strategy()
|
||||
```
|
||||
|
||||
**Resilience Features:**
|
||||
- **Exception isolation** preventing cascade failures
|
||||
- **Configurable thresholds** for minimum success requirements
|
||||
- **Comprehensive diagnostics** with actionable error context
|
||||
- **Automatic recovery** through provider substitution
|
||||
|
||||
## Configuration & Environment Management
|
||||
|
||||
### Sophisticated Configuration Hierarchy
|
||||
|
||||
**Environment-Driven Design:**
|
||||
```python
|
||||
# Secure, flexible configuration with validation
|
||||
class Config:
|
||||
@classmethod
|
||||
def validate_api_keys(cls) -> Dict[str, bool]:
|
||||
# Comprehensive credential validation across all providers
|
||||
|
||||
@classmethod
|
||||
def get_provider_config(cls, provider: str) -> Dict[str, Any]:
|
||||
# Dynamic configuration retrieval with intelligent defaults
|
||||
```
|
||||
|
||||
**Model Selection Matrix:**
|
||||
```python
|
||||
MODEL_MAPPINGS = {
|
||||
'openai-gpt5': ('openai', 'gpt-5'),
|
||||
'anthropic-opus4': ('anthropic', 'claude-opus-4-1-20250805'),
|
||||
'anthropic-sonnet4': ('anthropic', 'claude-sonnet-4-20250514'),
|
||||
'google-gemini25': ('google', 'gemini-2.5-pro')
|
||||
}
|
||||
```
|
||||
|
||||
## Quality Assurance & Validation Framework
|
||||
|
||||
### Comprehensive Verification Systems
|
||||
|
||||
**Expansion Validation:**
|
||||
- Mathematical verification of multiplier calculations against quantity targets
|
||||
- Semantic validation of language-country market combinations
|
||||
- Completeness verification ensuring no model findings are lost
|
||||
|
||||
**Consolidation Quality Metrics:**
|
||||
- Coverage analysis across all contributing models
|
||||
- Consistency scoring for multi-model agreement assessment
|
||||
- Deduplication effectiveness measurement
|
||||
|
||||
**Performance Monitoring:**
|
||||
- Individual model deliverable count tracking with average calculation
|
||||
- Processing time analysis across parallel execution
|
||||
- Cost efficiency metrics with provider-specific breakdowns
|
||||
- Token usage optimization through caching and context management
|
||||
|
||||
## CLI Interface & Operational Excellence
|
||||
|
||||
### Enhanced Command Interface
|
||||
|
||||
**Strategic Model Selection:**
|
||||
```bash
|
||||
# Maximum quality configuration
|
||||
--primary-models openai-gpt5,anthropic-opus4,google-gemini25 --consolidation-model anthropic-opus4
|
||||
|
||||
# Balanced performance (default)
|
||||
--primary-models openai-gpt5,anthropic-sonnet4,google-gemini25 --consolidation-model openai-gpt5
|
||||
|
||||
# Cost-optimized processing
|
||||
--primary-models openai-gpt5,google-gemini25 --consolidation-model google-gemini25
|
||||
```
|
||||
|
||||
**Operational Features:**
|
||||
- **Cost estimation** with user confirmation thresholds
|
||||
- **Model validation** with availability checking
|
||||
- **Comprehensive help** with usage examples and model descriptions
|
||||
- **Progress monitoring** with detailed processing stage logging
|
||||
|
||||
## Future Architecture & Extensibility
|
||||
|
||||
### Plugin-Ready Design
|
||||
|
||||
The system architecture supports seamless extension:
|
||||
- **Provider Addition**: Simple abstract class extension with automatic integration
|
||||
- **Schema Evolution**: External JSON-based schema management enabling hot-swapping
|
||||
- **Prompt Modification**: External template system supporting rapid iteration
|
||||
- **Configuration Enhancement**: Environment-based settings with validation frameworks
|
||||
|
||||
### Strategic Advantages
|
||||
|
||||
**Technical Excellence:**
|
||||
- Multi-model consensus achieving higher accuracy than individual model capabilities
|
||||
- Universal schema enabling provider interoperability without vendor lock-in
|
||||
- Mathematical precision in asset expansion preventing both under-counting and over-counting
|
||||
- Async architecture delivering performance optimization through true parallelism
|
||||
|
||||
**Operational Sophistication:**
|
||||
- Comprehensive cost management with multi-provider economic optimization
|
||||
- Enterprise-grade error handling with graceful degradation capabilities
|
||||
- Sophisticated monitoring providing operational transparency and debugging support
|
||||
- Configuration flexibility enabling deployment adaptation across environments
|
||||
|
||||
**Business Impact:**
|
||||
- Reliable asset extraction transforming project planning efficiency
|
||||
- Cost predictability through intelligent provider selection and budget controls
|
||||
- Quality assurance through multi-model validation and comprehensive verification
|
||||
- Scalable architecture supporting organizational growth and evolving requirements
|
||||
|
||||
---
|
||||
|
||||
**The Enhanced Brief Processing System v2.0: Where artificial intelligence meets architectural excellence to solve real-world business challenges with mathematical precision and operational reliability.**
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,847 +0,0 @@
|
|||
Below is a **hand‑off quality development plan** to add a complete, real‑time front end for your “marketing brief deliverable extraction” app. I’ve read through your codebase (providers, manager, analyzer, config, and schemas) and aligned the plan to what you already have so your developer can implement without guesswork.
|
||||
|
||||
---
|
||||
|
||||
## **0) What you already have (quick orientation)**
|
||||
|
||||
- **Providers (async-ready):**
|
||||
|
||||
- OpenAIProvider, AnthropicProvider, GoogleProvider implement a common async interface on top of BaseLLMProvider and return a standardized LLMResponse with token usage & timing. (Good: async and instrumentable.)
|
||||
|
||||
|
||||
- **Provider orchestration:**
|
||||
|
||||
- ProviderManager.execute_parallel_analysis(...) concurrently runs multiple providers and returns successful responses + metadata (counts, timing). (Good: true parallelism with asyncio.gather.)
|
||||
|
||||
|
||||
- **Analyzer / pipeline:**
|
||||
|
||||
- DocumentAnalyzer.process_document_multi_model(filepath):
|
||||
|
||||
1. classify + extract doc content (PowerPoint/Word/PDF/Excel)
|
||||
|
||||
2. run multi‑model analysis in parallel
|
||||
|
||||
3. consolidate model outputs into base deliverables
|
||||
|
||||
4. expand to final assets + generate CSV via generate_output_file(...)
|
||||
|
||||
|
||||
- It prints a CLI‑oriented summary and a few sentinel lines __COST_SUMMARY__, __TOKEN_USAGE__, __FILENAME__.
|
||||
|
||||
|
||||
- **Normalization & extraction intents** (what the UI should eventually show in a summary):
|
||||
|
||||
- The extraction schema and normalization rules live in your _universal schema_ and prompt files. The _multiplier-based extraction_ and _consolidation strategy_ are especially relevant to the “summary per file” you want to expose in the UI.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
> **Implication:** You already have the heavy lifting. What’s missing is: a web server, job queue, websocket progress events, and a clean React UI.
|
||||
|
||||
---
|
||||
|
||||
## **1) Target architecture (React/Vite + Quart/Hypercorn)**
|
||||
|
||||
|
||||
|
||||
**High-level flow**
|
||||
|
||||
```
|
||||
React (Vite/TS)
|
||||
├─ Upload one or many files → POST /api/jobs
|
||||
├─ Open WS → wss://.../ws (or /ws/<jobId>)
|
||||
├─ Shows a live Queue: each job card shows step + % + provider statuses
|
||||
├─ Collapsible summary for completed jobs (pulled from the pipeline results)
|
||||
└─ “Download CSV” appears when job completes → GET /api/jobs/<id>/download
|
||||
|
||||
Quart (Hypercorn)
|
||||
├─ POST /api/jobs - accept file(s), create Job(s), enqueue
|
||||
├─ GET /api/jobs - list active/recent jobs
|
||||
├─ GET /api/jobs/<id> - job detail (status/progress/result)
|
||||
├─ GET /api/jobs/<id>/download - send CSV result
|
||||
└─ WS /ws - broadcast queue+job updates (or /ws/<id> per job)
|
||||
|
||||
Async worker(s)
|
||||
├─ pulls from asyncio Queue, runs DocumentAnalyzer.process_document_multi_model()
|
||||
├─ emits granular progress events to WS (step transitions, model-by-model done)
|
||||
└─ stores per-job logs + summary + path to CSV
|
||||
```
|
||||
|
||||
**Tech choices**
|
||||
|
||||
- **Front end:** React + Vite + TypeScript + Tailwind (or CSS Modules) + Zustand (or Redux Toolkit) for global job state, + TanStack Query for REST endpoints.
|
||||
|
||||
- **Back end:** Quart + Hypercorn + asyncio queue and tasks. No Celery needed; your workloads are IO-bound (LLM calls) and already async.
|
||||
|
||||
- **Storage:** local disk for uploads & CSV outputs (S3/GCS is easy to swap later).
|
||||
|
||||
- **State:** in‑memory job registry + persisted artifacts (CSV + JSON summary). Optionally add SQLite later; not required for an MVP.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **2) Data contracts (server ↔ client)**
|
||||
|
||||
|
||||
|
||||
### **REST models**
|
||||
|
||||
```
|
||||
// JobStatus.ts
|
||||
export type JobPhase =
|
||||
| 'QUEUED'
|
||||
| 'EXTRACT_CONTENT'
|
||||
| 'LLM_ANALYSIS'
|
||||
| 'CONSOLIDATION'
|
||||
| 'CSV_GENERATION'
|
||||
| 'COMPLETED'
|
||||
| 'FAILED';
|
||||
|
||||
export interface ProviderUpdate {
|
||||
provider: 'openai' | 'anthropic' | 'google';
|
||||
model: string; // e.g., "gpt-5", "claude-3-5-sonnet", "gemini-2.5-pro"
|
||||
status: 'started' | 'success' | 'error';
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
latencyMs?: number;
|
||||
tokensIn?: number;
|
||||
tokensOut?: number;
|
||||
tokensCached?: number;
|
||||
costUsd?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface JobSummary {
|
||||
docType: string;
|
||||
assetsExtracted: number;
|
||||
confidenceScore: number;
|
||||
notes: string[]; // processing notes
|
||||
costUsdTotal: number;
|
||||
tokensTotal: number;
|
||||
primaryModels: string[];
|
||||
consolidationModel: string;
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id: string;
|
||||
fileName: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
phase: JobPhase;
|
||||
progressPct: number; // 0..100 (see weighting below)
|
||||
stepLabel: string; // human-friendly
|
||||
providerUpdates: Record<string, ProviderUpdate>;
|
||||
error?: string;
|
||||
resultCsvUrl?: string; // present when completed
|
||||
summary?: JobSummary; // present when completed
|
||||
}
|
||||
```
|
||||
|
||||
### **WebSocket events (JSON)**
|
||||
|
||||
```
|
||||
// Examples
|
||||
{ "type": "queue.snapshot", "jobs": Job[] }
|
||||
{ "type": "job.created", "job": Job }
|
||||
{ "type": "job.accepted", "jobId": "..." }
|
||||
{ "type": "job.progress", "jobId": "...", "phase": "LLM_ANALYSIS", "progressPct": 44, "message": "OpenAI done (1/3)", "providerUpdates": { "openai-gpt5": { /* ProviderUpdate */ } } }
|
||||
{ "type": "job.completed", "jobId": "...", "resultCsvUrl": "/api/jobs/123/download", "summary": JobSummary }
|
||||
{ "type": "job.failed", "jobId": "...", "error": "..." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **3) Backend (Quart/Hypercorn) — endpoints, queue, and instrumentation**
|
||||
|
||||
|
||||
|
||||
### **3.1 Project layout**
|
||||
|
||||
```
|
||||
server/
|
||||
app.py # Quart app factory
|
||||
jobs.py # Job dataclass, JobManager, in-memory registry
|
||||
ws.py # WebSocket manager (subscriptions, broadcast)
|
||||
runners.py # run_job(job) glue to your analyzer with progress hooks
|
||||
storage.py # upload & output directories, safe filenames
|
||||
config_runtime.py # CORS, max file size, concurrency limits
|
||||
# Your existing modules (keep them in a 'core' pkg or similar)
|
||||
core/
|
||||
process_brief_enhanced.py
|
||||
provider_manager.py
|
||||
base_provider.py
|
||||
openai_provider.py
|
||||
anthropic_provider.py
|
||||
google_provider.py
|
||||
config.py
|
||||
```
|
||||
|
||||
### **3.2 Job model & queue**
|
||||
|
||||
- Use an **asyncio.Queue** for pending work.
|
||||
|
||||
- A **concurrency semaphore** to cap parallel jobs (e.g., MAX_CONCURRENCY=2).
|
||||
|
||||
- A **JobRegistry** (dict) to track jobs by id.
|
||||
|
||||
- Each job keeps:
|
||||
|
||||
- phase, progress, per‑provider updates
|
||||
|
||||
- CSV path (when done)
|
||||
|
||||
- a rolling buffer of **log lines** (for the collapsible detail in the UI)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
> The existing pipeline logs useful milestones (“=== STAGE 2: …”, “Consolidation completed…”, asset counts, costs, token usage). We’ll **capture those logs per job** with a custom logging Handler (see 3.4).
|
||||
|
||||
|
||||
|
||||
### **3.3 REST endpoints (async)**
|
||||
|
||||
- POST /api/jobs
|
||||
|
||||
- Multipart form; one or many files in files[].
|
||||
|
||||
- For each file: create a Job, store upload in /data/uploads/<jobId>_<safeName>, enqueue.
|
||||
|
||||
- Return { jobs: Job[] } with initial phase QUEUED.
|
||||
|
||||
|
||||
- GET /api/jobs — return active + recent jobs (paged).
|
||||
|
||||
- GET /api/jobs/<id> — return a single job.
|
||||
|
||||
- GET /api/jobs/<id>/download — stream the CSV using quart.send_file(...).
|
||||
|
||||
- (Optional) POST /api/jobs/<id>/cancel, POST /api/jobs/<id>/retry.
|
||||
|
||||
|
||||
|
||||
|
||||
### **3.4 WebSocket broadcast**
|
||||
|
||||
- GET /ws (or /ws/<jobId>):
|
||||
|
||||
- Clients connect once; server pushes queue.snapshot then targeted events.
|
||||
|
||||
|
||||
- Maintain a **WebSocketManager**:
|
||||
|
||||
- subscribe(client) / unsubscribe(client)
|
||||
|
||||
- broadcast(eventJson) and broadcast_job(jobId, eventJson)
|
||||
|
||||
|
||||
- To keep this simple: broadcast all events to all clients; the front end filters by user session if needed.
|
||||
|
||||
|
||||
|
||||
|
||||
### **3.5 Instrument the pipeline for live progress**
|
||||
|
||||
|
||||
|
||||
We will **not** rewrite your core logic; we’ll **decorate** it with progress hooks:
|
||||
|
||||
|
||||
|
||||
**Progress weights → single %:**
|
||||
|
||||
- EXTRACT_CONTENT — 25%
|
||||
|
||||
- LLM_ANALYSIS — 50% (divide evenly across N providers; update after each provider returns)
|
||||
|
||||
- CONSOLIDATION — 15%
|
||||
|
||||
- CSV_GENERATION — 10%
|
||||
|
||||
|
||||
|
||||
|
||||
**Emitters:**
|
||||
|
||||
- Add a small **ProgressReporter** interface:
|
||||
|
||||
|
||||
```
|
||||
# runners.py
|
||||
class ProgressReporter:
|
||||
def __init__(self, job, ws_manager):
|
||||
self.job = job
|
||||
self.ws = ws_manager
|
||||
|
||||
async def emit(self, phase: str, pct: float, message: str = "", provider_update: dict | None = None):
|
||||
# Update the in-memory registry
|
||||
self.job.phase = phase
|
||||
self.job.progress_pct = pct
|
||||
self.job.step_label = {
|
||||
'EXTRACT_CONTENT': 'Extracting document content',
|
||||
'LLM_ANALYSIS': 'Parallel LLM analysis',
|
||||
'CONSOLIDATION': 'Consolidating results',
|
||||
'CSV_GENERATION': 'Generating CSV',
|
||||
'COMPLETED': 'Completed',
|
||||
'FAILED': 'Failed'
|
||||
}[phase]
|
||||
if provider_update:
|
||||
self.job.provider_updates[provider_update['model_key']] = provider_update
|
||||
|
||||
self.job.updated_at = datetime.utcnow().isoformat()
|
||||
# Broadcast
|
||||
await self.ws.broadcast_job(self.job.id, {
|
||||
"type": "job.progress",
|
||||
"jobId": self.job.id,
|
||||
"phase": phase,
|
||||
"progressPct": int(pct),
|
||||
"message": message,
|
||||
"providerUpdates": provider_update and { provider_update["model_key"]: provider_update } or {}
|
||||
})
|
||||
```
|
||||
|
||||
**Hook points:**
|
||||
|
||||
- In DocumentAnalyzer.process_document_multi_model(...):
|
||||
|
||||
- Before/after content extraction → emit('EXTRACT_CONTENT', ~10 → ~25, ...).
|
||||
|
||||
- After _perform_parallel_analysis(...) starts, the **per‑provider updates** will come from ProviderManager (see below).
|
||||
|
||||
- Before/after consolidation → emit('CONSOLIDATION', ~80 → ~90, ...).
|
||||
|
||||
- After CSV write → emit('CSV_GENERATION', 95), then COMPLETED.
|
||||
|
||||
|
||||
- In ProviderManager.execute_parallel_analysis(...):
|
||||
|
||||
- Accept an optional on_model_event(model_key: str, stage: 'start'|'end', response_or_exc) callback, called:
|
||||
|
||||
- **start:** immediately before provider.generate_response(...)
|
||||
|
||||
- **end:** as each task returns (success or failure)
|
||||
|
||||
|
||||
- Use it to:
|
||||
|
||||
- mark a provider as started (with timestamp),
|
||||
|
||||
- on success: compute latency/tokens/cost (use provider.estimate_cost with LLMResponse.token_usage) and mark success,
|
||||
|
||||
- on failure: mark error.
|
||||
|
||||
|
||||
- After each provider finishes, compute partial % for LLM_ANALYSIS like:
|
||||
|
||||
- analysisBase = 25 → 25 + (completedProviders / totalProviders) * 50.
|
||||
|
||||
|
||||
|
||||
- **Per‑job structured logging:**
|
||||
|
||||
- Create a logging.Handler subclass that writes lines into job.logs buffer **and** optionally emits job.log WS events the UI can append to a collapsible panel.
|
||||
|
||||
- Use a contextvars.ContextVar[current_job_id] set by the job runner, and add a filter that attaches the current job id to every log record.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
> **Why this matters:** Your analyzer already prints detailed milestones such as “=== STAGE 2: Starting Parallel Multi-Model Analysis ===”, the consolidation counts, extracted asset counts, cost tokens, etc. Capturing them as _structured job logs_ is the easiest way to satisfy the “summary from the logs” requirement without refactoring the algorithms.
|
||||
|
||||
|
||||
|
||||
### **3.6 CSV & summary surfacing**
|
||||
|
||||
- Reuse generate_output_file(...) (already writes a CSV from results.raw_data).
|
||||
|
||||
- On completion:
|
||||
|
||||
- Persist “summary” in memory and as a small JSON file next to the CSV. Set job.resultCsvUrl.
|
||||
|
||||
- The summary fields come straight from your ProcessingResult + consolidation metadata:
|
||||
|
||||
- docType (results.metadata['doc_type'])
|
||||
|
||||
- assetsExtracted (len(results.raw_data))
|
||||
|
||||
- confidenceScore
|
||||
|
||||
- processingNotes (list)
|
||||
|
||||
- totalCost & totalTokens (you already compute them and log; collect them in code rather than via print)
|
||||
|
||||
- models used (primary_models, consolidation_model)
|
||||
|
||||
|
||||
- Broadcast a job.completed event carrying resultCsvUrl and summary.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### **3.7 Minor backend adjustments to support the UI cleanly**
|
||||
|
||||
1. **ProviderManager:**
|
||||
|
||||
- Add an optional on_model_event callback to execute_parallel_analysis(...).
|
||||
|
||||
- Compute per‑provider cost via provider.estimate_cost(...) once a response comes back.
|
||||
|
||||
|
||||
2. **DocumentAnalyzer:**
|
||||
|
||||
- Accept an optional progress: ProgressReporter on process_document_multi_model(...).
|
||||
|
||||
- Call progress.emit(...) at phase boundaries (see 3.5).
|
||||
|
||||
- Avoid printing the legacy __COST_SUMMARY__, __TOKEN_USAGE__, __FILENAME__; add these values to the returned ProcessingResult so the REST/WS layer can use them.
|
||||
|
||||
|
||||
3. **Prompts & schema paths:**
|
||||
|
||||
- Your analyzer loads prompts/universal_schema.json, prompts/multi_perspective_analysis.txt, etc. Ensure those files are located accordingly in deployment. The **universal schema** shapes the asset objects you show in summaries/downloads.
|
||||
|
||||
- The **extraction** and **consolidation** prompt files should remain alongside and continue to normalize titles/categories/specs as your UI will lean on those fields.
|
||||
|
||||
|
||||
4. **Config:**
|
||||
|
||||
- Add **CORS** whitelist for the front end origin.
|
||||
|
||||
- Runtime knobs: MAX_CONCURRENCY, MAX_UPLOAD_MB, KEEP_DAYS for temp files, WS_PING_INTERVAL, etc.
|
||||
|
||||
|
||||
5. **Hypercorn:**
|
||||
|
||||
- Provide hypercorn.toml with reasonable defaults (workers=1–2 async workers, websocket enabled).
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **4) Front end (React/Vite/TS) — screens, state, and UX**
|
||||
|
||||
|
||||
|
||||
### **4.1 Pages & components**
|
||||
|
||||
- **App shell**: Header (logo, dark mode toggle), main content (Upload + Queue), footer.
|
||||
|
||||
- **UploadPanel**
|
||||
|
||||
- Drag‑and‑drop + multi‑select file input.
|
||||
|
||||
- Show pending files before submission; allow remove.
|
||||
|
||||
- On submit, POST /api/jobs, then clear the local staging list.
|
||||
|
||||
|
||||
- **QueueView**
|
||||
|
||||
- **Active Jobs** list (reverse chronological). Each **JobCard** shows:
|
||||
|
||||
- File name
|
||||
|
||||
- **ProgressBar** + stepLabel
|
||||
|
||||
- Per‑provider chips: started/success/error + latency & tiny token count tooltip
|
||||
|
||||
- Cancel (optional), retry on failure
|
||||
|
||||
|
||||
- **Completed** list (collapsible group)
|
||||
|
||||
- Each **JobAccordion** opens to a **SummaryPanel**
|
||||
|
||||
- Document type, assets extracted, confidence score, total cost, total tokens, list of notes (bulleted)
|
||||
|
||||
- “Download CSV” button (direct to /api/jobs/<id>/download)
|
||||
|
||||
- **LogsPanel**: streaming or captured log lines with a filter (“info/error”)
|
||||
|
||||
|
||||
|
||||
|
||||
- **ConnectionStatus** (WS up/down indicator, auto‑retry).
|
||||
|
||||
|
||||
|
||||
|
||||
### **4.2 Client‑side state management**
|
||||
|
||||
- **Zustand store** for jobs keyed by id.
|
||||
|
||||
- On WS connection:
|
||||
|
||||
- Apply queue.snapshot.
|
||||
|
||||
- Merge incremental job.* events.
|
||||
|
||||
|
||||
- Use **TanStack Query** for on‑demand REST fetches (/api/jobs, /api/jobs/<id>).
|
||||
|
||||
|
||||
|
||||
|
||||
### **4.3 Realtime updates (websocket)**
|
||||
|
||||
- On app mount, connect to /ws.
|
||||
|
||||
- Reconnect with exponential backoff if disconnected.
|
||||
|
||||
- Debounce UI updates (e.g., batch events every 100ms) to keep the UI smooth.
|
||||
|
||||
|
||||
|
||||
|
||||
### **4.4 Polished UX touches**
|
||||
|
||||
- “Copy path” / “Copy CSV to clipboard” quick actions after completion.
|
||||
|
||||
- Show **estimated cost** as providers complete (sum of known providers).
|
||||
|
||||
- **Model badges** (GPT‑5 / Sonnet / Gemini) with tooltips.
|
||||
|
||||
- Persist recent jobs in sessionStorage so a refresh doesn’t flash empty.
|
||||
|
||||
- Global **error boundary** (human‑readable fallback).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **5) Progress math (so % never jumps backwards)**
|
||||
|
||||
- Initialize QUEUED = 0%.
|
||||
|
||||
- EXTRACT_CONTENT: set to 10% at start; 25% when done.
|
||||
|
||||
- LLM_ANALYSIS: base = 25%. If M providers, each success/fail completion adds 50 / M. If at least N successes are required (config: MINIMUM_SUCCESS_THRESHOLD), mark phase as done (75%) when either all finished or threshold is met and others failed.
|
||||
|
||||
- CONSOLIDATION: 75% → 90% when done.
|
||||
|
||||
- CSV_GENERATION: 90% → 100% when CSV saved.
|
||||
|
||||
- Never decrement; clamp to [0,100].
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **6) Security & operations**
|
||||
|
||||
- **CORS**: allow only your front end origin(s).
|
||||
|
||||
- **Upload validation**: restrict to allowed MIME/types (pptx, docx, pdf, xls[x]); size limit; generate a safe server filename.
|
||||
|
||||
- **Sandbox**: process in a temp dir per job; delete original file on completion (keep CSV + summary only) if desired.
|
||||
|
||||
- **Secrets**: use environment variables (.env) already supported by your config.py.
|
||||
|
||||
- **Resource limits**: concurrency semaphore; Hypercorn timeouts; WS ping keep‑alive.
|
||||
|
||||
- **Logging**: rotate application logs; per‑job logs are kept with the artifacts for later audit.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **7) Testing plan**
|
||||
|
||||
- **Unit tests**
|
||||
|
||||
- Progress computation for M providers (1, 2, 3…) with different success/failure mixes.
|
||||
|
||||
- ProviderManager callback flow (start/end events, cost calculations).
|
||||
|
||||
- DocumentAnalyzer emits correct phase transitions even on exceptions.
|
||||
|
||||
|
||||
- **Integration tests**
|
||||
|
||||
- POST multi‑upload → receive WS job.created → see steady job.progress → job.completed, then CSV download returns file.
|
||||
|
||||
- Fault injection: missing API key → model error visible in provider chip; job still succeeds if threshold met.
|
||||
|
||||
|
||||
- **Manual regression**
|
||||
|
||||
- Very large PDFs, multiple PPTX simultaneously, doc type misclassification.
|
||||
|
||||
- WS reconnect mid‑job; browser refresh while job in progress.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **8) Developer task breakdown (do in this order)**
|
||||
|
||||
1. **Scaffold the server**
|
||||
|
||||
- Quart app factory, Hypercorn config, CORS.
|
||||
|
||||
- File storage dirs (/data/uploads, /data/outputs).
|
||||
|
||||
|
||||
2. **Job system**
|
||||
|
||||
- Job, JobManager, registry, queue, worker task(s).
|
||||
|
||||
|
||||
3. **WebSocket manager**
|
||||
|
||||
- /ws endpoint; queue.snapshot broadcast on connect.
|
||||
|
||||
|
||||
4. **Wire the runner**
|
||||
|
||||
- run_job(job, progress) that:
|
||||
|
||||
- calls DocumentAnalyzer.process_document_multi_model(path, progress=...),
|
||||
|
||||
- sets resultCsvUrl and summary,
|
||||
|
||||
- emits job.completed or job.failed.
|
||||
|
||||
|
||||
|
||||
5. **Instrument providers**
|
||||
|
||||
- Add on_model_event to ProviderManager.execute_parallel_analysis.
|
||||
|
||||
- Compute provider costs from LLMResponse.token_usage.
|
||||
|
||||
|
||||
6. **Instrument analyzer**
|
||||
|
||||
- Call progress.emit(...) at each phase boundary.
|
||||
|
||||
- Return a structured ProcessingResult with cost/tokens so the runner can fill summary without parsing console prints.
|
||||
|
||||
|
||||
7. **REST endpoints** (POST /api/jobs, GET /api/jobs, GET /api/jobs/<id>, GET /api/jobs/<id>/download)
|
||||
|
||||
8. **Front end scaffold**
|
||||
|
||||
- Vite + React + TS + Tailwind; set base URL from env.
|
||||
|
||||
|
||||
9. **WS client & store**
|
||||
|
||||
- Zustand store + WS connection + event reducers.
|
||||
|
||||
|
||||
10. **UI components**
|
||||
|
||||
- Upload panel → POST to create jobs.
|
||||
|
||||
- Queue view with JobCard/ProgressBar/Provider chips.
|
||||
|
||||
- Completed accordion with Summary & Logs & Download.
|
||||
|
||||
|
||||
11. **Polish**
|
||||
|
||||
- Error states, retries, loading skeletons, toasts.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **9) Code snippets your developer can drop in**
|
||||
|
||||
|
||||
|
||||
### **9.1 Quart WS & job queue skeleton**
|
||||
|
||||
```
|
||||
# app.py
|
||||
from quart import Quart, websocket, request, jsonify, send_file
|
||||
import asyncio
|
||||
from jobs import JobManager
|
||||
from ws import WebSocketManager
|
||||
|
||||
def create_app():
|
||||
app = Quart(__name__)
|
||||
app.config.from_mapping(MAX_CONTENT_LENGTH=1024 * 1024 * 200) # 200MB
|
||||
|
||||
jm = JobManager()
|
||||
ws = WebSocketManager()
|
||||
|
||||
@app.websocket('/ws')
|
||||
async def ws_handler():
|
||||
client = ws.register()
|
||||
try:
|
||||
# send snapshot on connect
|
||||
await ws.send(client, {"type": "queue.snapshot", "jobs": jm.serialize_all()})
|
||||
async for message in websocket:
|
||||
pass # no incoming messages needed yet
|
||||
finally:
|
||||
ws.unregister(client)
|
||||
|
||||
@app.post('/api/jobs')
|
||||
async def create_jobs():
|
||||
files = await request.files
|
||||
created = []
|
||||
for _, f in files.items():
|
||||
job = await jm.create_job_from_upload(f, ws)
|
||||
created.append(job.to_dict())
|
||||
return jsonify({"jobs": created})
|
||||
|
||||
@app.get('/api/jobs')
|
||||
async def list_jobs():
|
||||
return jsonify({"jobs": jm.serialize_all()})
|
||||
|
||||
@app.get('/api/jobs/<job_id>')
|
||||
async def get_job(job_id):
|
||||
job = jm.get(job_id)
|
||||
if not job:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
return jsonify(job.to_dict())
|
||||
|
||||
@app.get('/api/jobs/<job_id>/download')
|
||||
async def download(job_id):
|
||||
job = jm.get(job_id)
|
||||
if not job or not job.result_csv_path:
|
||||
return jsonify({"error": "not ready"}), 400
|
||||
return await send_file(job.result_csv_path, as_attachment=True)
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
### **9.2 ProviderManager callback (excerpt)**
|
||||
|
||||
```
|
||||
# provider_manager.py (inside execute_parallel_analysis)
|
||||
async def execute_parallel_analysis(..., on_model_event=None, ...):
|
||||
...
|
||||
for model_key in valid_model_keys:
|
||||
provider = self.get_provider(model_key)
|
||||
async def run_one(model_key=model_key, provider=provider):
|
||||
if on_model_event:
|
||||
await on_model_event(model_key, 'start', None)
|
||||
try:
|
||||
resp = await provider.generate_response(messages, schema)
|
||||
if on_model_event:
|
||||
# cost calc here avoids duplicating later
|
||||
cost = provider.estimate_cost(resp.token_usage.input_tokens,
|
||||
resp.token_usage.output_tokens,
|
||||
resp.token_usage.cached_input_tokens)
|
||||
await on_model_event(model_key, 'end', {"response": resp, "cost": cost})
|
||||
return resp
|
||||
except Exception as e:
|
||||
if on_model_event:
|
||||
await on_model_event(model_key, 'end', {"error": str(e)})
|
||||
raise
|
||||
tasks.append((model_key, asyncio.create_task(run_one())))
|
||||
...
|
||||
```
|
||||
|
||||
### **9.3 Runner wiring progress into analyzer**
|
||||
|
||||
```
|
||||
# runners.py
|
||||
from core.process_brief_enhanced import DocumentAnalyzer
|
||||
from datetime import datetime
|
||||
|
||||
async def run_job(job, ws_manager):
|
||||
progress = ProgressReporter(job, ws_manager)
|
||||
analyzer = DocumentAnalyzer()
|
||||
try:
|
||||
await progress.emit('EXTRACT_CONTENT', 10, 'Starting content extraction')
|
||||
# analyzer handles the rest but we’ll pass callbacks down
|
||||
results = await analyzer.process_document_multi_model(
|
||||
job.upload_path,
|
||||
progress=progress, # extend the signature to pass reporter
|
||||
on_model_event=lambda *args, **kwargs: on_model_event_bridge(progress, job, *args, **kwargs)
|
||||
)
|
||||
# Wrap up
|
||||
job.result_csv_path = generate_output_file_path_from(results) # or results already contains path
|
||||
job.summary = make_summary_from(results)
|
||||
await progress.emit('CSV_GENERATION', 100, 'CSV ready')
|
||||
await ws_manager.broadcast_job(job.id, {"type": "job.completed", "jobId": job.id, "resultCsvUrl": job.result_csv_url, "summary": job.summary})
|
||||
except Exception as e:
|
||||
job.error = str(e)
|
||||
await ws_manager.broadcast_job(job.id, {"type": "job.failed", "jobId": job.id, "error": job.error})
|
||||
```
|
||||
|
||||
> Your developer will add matching progress hooks within DocumentAnalyzer (stage boundaries) and call the on_model_event bridge when each provider starts/finishes inside ProviderManager.
|
||||
|
||||
---
|
||||
|
||||
## **10) “Summary from the logs” (what exactly to show)**
|
||||
|
||||
|
||||
|
||||
Use the values your pipeline already computes and logs:
|
||||
|
||||
- **Doc type** (results.metadata['doc_type'])
|
||||
|
||||
- **Assets extracted** (len(results.raw_data))
|
||||
|
||||
- **Confidence score** (results.confidence_score)
|
||||
|
||||
- **Processing notes** (results.processing_notes)
|
||||
|
||||
- **Token usage / cost**
|
||||
|
||||
- Use the aggregated token usage already returned and the model cost methods you implemented.
|
||||
|
||||
|
||||
- **Models used** (results.metadata['primary_models_used'], results.metadata['consolidation_model'])
|
||||
|
||||
|
||||
|
||||
|
||||
Your prompts and schema explicitly drive normalization and multipliers (what the user ultimately cares about in the CSV), which is why it’s helpful to surface them in the UI description or tooltips. The **extraction method** and **title/category normalization** are captured in your prompt files and schema: multiplier arrays for specs and markets, and normalized titles/categories without multipliers (what users will see consistently in the final data). The **consolidation strategy** (inclusive + normalized + deduped) explains why counts sometimes adjust between models and the final, and is a great addition to an “info” tooltip in the UI.
|
||||
|
||||
---
|
||||
|
||||
## **11) Nice-to-haves that will make your GUI feel “pro”**
|
||||
|
||||
- **Per‑job “estimated time/cost”** pane that updates as providers complete (based on known provider costs and elapsed).
|
||||
|
||||
- **Download “all CSVs (zip)”** for a batch upload.
|
||||
|
||||
- **Retry failed providers** without re-uploading the file (e.g., re-run just Gemini if it errored).
|
||||
|
||||
- **User-configurable models/thresholds** (choose models, set MINIMUM_SUCCESS_THRESHOLD) in an “Advanced” drawer.
|
||||
|
||||
- **Accessible UI**: semantic progress bars, screen‑reader text for step labels.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **12) Definition of Done (DoD)**
|
||||
|
||||
- Upload single or multiple files and see them **enter the queue** instantly.
|
||||
|
||||
- For each job:
|
||||
|
||||
- **Live step** (“Extracting content”, “Parallel LLM analysis”, “Consolidating”, “Generating CSV”) and a **non‑jittery %**.
|
||||
|
||||
- **Per‑provider chips** showing started → success/error, with latency/tokens on hover.
|
||||
|
||||
- A **collapsible summary** populated from the pipeline (doc type, assets extracted, confidence, notes, cost, tokens, models).
|
||||
|
||||
- **Download CSV** button becomes active at completion; file downloads to the user’s computer.
|
||||
|
||||
|
||||
- WS reconnect works; no duplicated entries; no memory growth with many jobs.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **Appendix: why we cite these prompt/schema files**
|
||||
|
||||
- The **consolidation** rules (inclusive, normalized, deduped) from consolidation_analysis.txt dictate how final items are merged and what the user should expect in counts vs. inputs, so it’s relevant for the UI’s “what happened” notes.
|
||||
|
||||
- The **extraction method** and **field normalization** from multi_perspective_analysis.txt are the backbone of consistent asset fields that your UI will show in summaries and that go into the CSV.
|
||||
|
||||
- The **universal schema** defines the shape of the extracted **base deliverables** with multiplier arrays (technical specs, markets), which explains the CSV columns and why some fields are arrays vs strings.
|
||||
|
||||
|
||||
---
|
||||
|
||||
If you want, I can next produce a small **reference implementation** (minimal Quart app + a mocked runner that emits realistic progress) so your developer can plug your real analyzer in and iterate quickly.
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +0,0 @@
|
|||
# Backend API URL Configuration
|
||||
# Copy this file to .env and configure for your environment
|
||||
|
||||
# Production deployment
|
||||
VITE_API_URL=https://ai-sandbox.oliver.solutions/brief-extractor-back/api
|
||||
VITE_WS_URL=wss://ai-sandbox.oliver.solutions/brief-extractor-back
|
||||
|
||||
# Local development
|
||||
# VITE_API_URL=http://localhost:8000/api
|
||||
# VITE_WS_URL=ws://localhost:8000
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
# Frontend Deployment Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Configure Base Path (if needed)
|
||||
The frontend is configured to be hosted at `/brief-extractor/` (see `vite.config.ts` - `base` property).
|
||||
If you need to deploy to a different path, edit `vite.config.ts` and change the `base` value.
|
||||
|
||||
### 2. Configure Environment
|
||||
Edit `frontend/.env` to set your backend URLs:
|
||||
|
||||
**Production (Current Configuration):**
|
||||
```bash
|
||||
VITE_API_URL=https://ai-sandbox.oliver.solutions/brief-extractor-back/api
|
||||
VITE_WS_URL=wss://ai-sandbox.oliver.solutions/brief-extractor-back
|
||||
```
|
||||
|
||||
**Local Development:**
|
||||
```bash
|
||||
VITE_API_URL=http://localhost:8000/api
|
||||
VITE_WS_URL=ws://localhost:8000
|
||||
```
|
||||
|
||||
### 3. Build for Production
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install # Install dependencies (if not already done)
|
||||
npm run build # Creates optimized build in dist/
|
||||
```
|
||||
|
||||
### 4. Deploy
|
||||
|
||||
Upload the contents of `frontend/dist/` to your web server at the `/brief-extractor/` path.
|
||||
|
||||
**Important:** The frontend expects to be served from `https://your-domain.com/brief-extractor/`
|
||||
- The built files reference assets with `/brief-extractor/assets/...` paths
|
||||
- If deploying to a different path, update `base` in `vite.config.ts` and rebuild
|
||||
|
||||
## Switching Between Environments
|
||||
|
||||
The frontend is configured using environment variables that are **embedded at build time**. This means you need to rebuild the application when switching between environments.
|
||||
|
||||
### To Build for Server Deployment
|
||||
|
||||
1. Edit `frontend/.env`:
|
||||
```bash
|
||||
# Uncomment production URLs
|
||||
VITE_API_URL=https://ai-sandbox.oliver.solutions/brief-extractor-back/api
|
||||
VITE_WS_URL=wss://ai-sandbox.oliver.solutions/brief-extractor-back
|
||||
|
||||
# Comment out local URLs
|
||||
# VITE_API_URL=http://localhost:8000/api
|
||||
# VITE_WS_URL=ws://localhost:8000
|
||||
```
|
||||
|
||||
2. Build:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
3. Deploy `dist/` directory to your server
|
||||
|
||||
### To Build for Local Development
|
||||
|
||||
1. Edit `frontend/.env`:
|
||||
```bash
|
||||
# Comment out production URLs
|
||||
# VITE_API_URL=https://ai-sandbox.oliver.solutions/brief-extractor-back/api
|
||||
# VITE_WS_URL=wss://ai-sandbox.oliver.solutions/brief-extractor-back
|
||||
|
||||
# Uncomment local URLs
|
||||
VITE_API_URL=http://localhost:8000/api
|
||||
VITE_WS_URL=ws://localhost:8000
|
||||
```
|
||||
|
||||
2. Run dev server (no build needed):
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
After building, verify the correct URLs are embedded:
|
||||
|
||||
```bash
|
||||
# Check if production URL is in the build
|
||||
grep -o "ai-sandbox.oliver.solutions" dist/assets/index-*.js
|
||||
|
||||
# Should output the domain if production build is correct
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- **VITE_API_URL**: Base URL for REST API endpoints (includes `/api` path)
|
||||
- **VITE_WS_URL**: Base URL for WebSocket connections (no `/ws` path, it's added by the client)
|
||||
|
||||
## Build Output
|
||||
|
||||
The build creates static files in `frontend/dist/`:
|
||||
- `index.html` - Main HTML file
|
||||
- `assets/` - JavaScript, CSS, and other assets with hashed filenames
|
||||
|
||||
These files can be served by any static file server (nginx, Apache, etc.).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Wrong backend URL after deployment
|
||||
- **Problem**: Frontend is trying to connect to localhost instead of production server
|
||||
- **Solution**: Check `.env` file has production URLs, then rebuild with `npm run build`
|
||||
|
||||
### WebSocket connection fails
|
||||
- **Problem**: wss:// connection fails or shows certificate errors
|
||||
- **Solution**: Ensure your server supports WebSocket connections over SSL and the path `/brief-extractor-back/ws` is correctly proxied to the backend
|
||||
|
||||
### API calls return 404
|
||||
- **Problem**: REST API endpoints not found
|
||||
- **Solution**: Verify the backend is running at the configured URL and the `/api` path is correctly handled
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build application
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy built files
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy custom nginx configuration
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
# Brief Extractor Frontend
|
||||
|
||||
React-based frontend for the Enhanced Brief Processing System.
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
The frontend uses environment variables to configure the backend API and WebSocket URLs.
|
||||
|
||||
### Setup
|
||||
|
||||
1. Copy `.env.example` to `.env`:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Edit `.env` to configure for your target environment:
|
||||
|
||||
**For Production Deployment:**
|
||||
```bash
|
||||
VITE_API_URL=https://ai-sandbox.oliver.solutions/brief-extractor-back/api
|
||||
VITE_WS_URL=wss://ai-sandbox.oliver.solutions/brief-extractor-back
|
||||
```
|
||||
|
||||
**For Local Development:**
|
||||
```bash
|
||||
VITE_API_URL=http://localhost:8000/api
|
||||
VITE_WS_URL=ws://localhost:8000
|
||||
```
|
||||
|
||||
### Building for Different Environments
|
||||
|
||||
The environment variables are embedded at build time, so you need to configure `.env` before running the build:
|
||||
|
||||
**Build for Production:**
|
||||
```bash
|
||||
# Ensure .env has production URLs
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Build for Local Development:**
|
||||
```bash
|
||||
# Ensure .env has local URLs
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Run Development Server:**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The dev server will use the URLs from `.env` or fall back to the proxy configuration in `vite.config.ts`.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `npm run dev` - Start development server on port 3000
|
||||
- `npm run build` - Build for production (uses URLs from `.env`)
|
||||
- `npm run preview` - Preview production build locally
|
||||
- `npm run lint` - Run ESLint
|
||||
- `npm run type-check` - Run TypeScript type checking
|
||||
|
||||
## Deployment
|
||||
|
||||
When deploying to production:
|
||||
|
||||
1. Ensure `.env` has the correct production URLs
|
||||
2. Run `npm run build`
|
||||
3. Deploy the `dist/` directory to your web server
|
||||
|
||||
The built files in `dist/` will have the backend URLs baked in based on the `.env` configuration at build time.
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
# WebSocket Configuration Guide
|
||||
|
||||
## Current Status
|
||||
|
||||
The frontend **gracefully handles WebSocket failures** and will continue to work without real-time updates. WebSockets are optional for the app to function.
|
||||
|
||||
## What WebSockets Provide
|
||||
|
||||
- **Real-time job status updates** - See progress without refreshing
|
||||
- **Live log streaming** - View processing logs as they happen
|
||||
- **Instant notifications** - Get notified when jobs complete
|
||||
|
||||
## Without WebSockets
|
||||
|
||||
The app will still work perfectly fine by:
|
||||
- **Polling for updates** - Automatically checks job status periodically
|
||||
- **Manual refresh** - User can refresh to see latest status
|
||||
- **All functionality preserved** - Upload, process, download all work normally
|
||||
|
||||
## Setting Up WebSocket Support (Optional)
|
||||
|
||||
If you want real-time updates, configure your web server to proxy WebSocket connections:
|
||||
|
||||
### Nginx Configuration
|
||||
|
||||
Add to your nginx site configuration:
|
||||
|
||||
```nginx
|
||||
# HTTP API proxy
|
||||
location /brief-extractor-back/api {
|
||||
proxy_pass http://localhost:8000/api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# WebSocket proxy
|
||||
location /brief-extractor-back/ws {
|
||||
proxy_pass http://localhost:8000/ws;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
```
|
||||
|
||||
### Apache Configuration
|
||||
|
||||
Add to your Apache virtual host:
|
||||
|
||||
```apache
|
||||
# HTTP API proxy
|
||||
ProxyPass /brief-extractor-back/api http://localhost:8000/api
|
||||
ProxyPassReverse /brief-extractor-back/api http://localhost:8000/api
|
||||
|
||||
# WebSocket proxy
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/brief-extractor-back/ws/(.*) ws://localhost:8000/ws/$1 [P,L]
|
||||
|
||||
ProxyPass /brief-extractor-back/ws ws://localhost:8000/ws
|
||||
ProxyPassReverse /brief-extractor-back/ws ws://localhost:8000/ws
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### WebSocket fails with NS_ERROR_WEBSOCKET_CONNECTION_REFUSED
|
||||
|
||||
**This is expected if:**
|
||||
- Backend is not running
|
||||
- Web server doesn't have WebSocket proxy configured
|
||||
- Firewall blocking WebSocket connections
|
||||
|
||||
**The app will:**
|
||||
- Show "WebSocket connection failed" message once in console
|
||||
- Automatically stop trying after 3 attempts (to reduce noise)
|
||||
- Continue working normally without real-time updates
|
||||
- Check periodically (every 30s) if WebSocket becomes available
|
||||
|
||||
### Console shows many WebSocket errors
|
||||
|
||||
The latest build reduces console noise significantly:
|
||||
- Only logs the first connection failure
|
||||
- Stops retrying after 3 attempts (was 10)
|
||||
- Uses longer retry intervals (5s → 60s vs 1s → 30s)
|
||||
- Silent background health checks
|
||||
|
||||
## Testing WebSocket Connection
|
||||
|
||||
In browser console:
|
||||
```javascript
|
||||
// Check if WebSocket is connected
|
||||
wsClient.isConnected()
|
||||
|
||||
// Check connection state
|
||||
wsClient.getConnectionState()
|
||||
|
||||
// Force reconnection attempt
|
||||
wsClient.forceReconnect()
|
||||
```
|
||||
|
||||
## Current Configuration
|
||||
|
||||
- **Frontend expects WebSocket at:** `wss://ai-sandbox.oliver.solutions/brief-extractor-back/ws`
|
||||
- **Retry attempts:** 3 (then stops)
|
||||
- **Initial retry delay:** 5 seconds
|
||||
- **Max retry delay:** 60 seconds
|
||||
- **Health check interval:** 30 seconds (when disconnected)
|
||||
|
||||
## Recommendation
|
||||
|
||||
For production deployment, **WebSockets are nice to have but not required**. You can:
|
||||
|
||||
1. **Deploy without WebSockets** - App works fine, users just need to refresh
|
||||
2. **Add WebSockets later** - Configure web server proxy when you have time
|
||||
3. **Use polling instead** - Backend could implement polling endpoint as alternative
|
||||
|
||||
The current build is optimized to fail gracefully and not spam errors when WebSockets are unavailable.
|
||||
1
frontend/dist/assets/index-DDOzbOUM.css
vendored
1
frontend/dist/assets/index-DDOzbOUM.css
vendored
File diff suppressed because one or more lines are too long
15
frontend/dist/index.html
vendored
15
frontend/dist/index.html
vendored
|
|
@ -1,15 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Brief Extractor GUI</title>
|
||||
<meta name="description" content="AI-powered document analysis for marketing asset extraction" />
|
||||
<script type="module" crossorigin src="/brief-extractor/assets/index-C4aSWl2m.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/brief-extractor/assets/index-DDOzbOUM.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Brief Extractor GUI</title>
|
||||
<meta name="description" content="AI-powered document analysis for marketing asset extraction" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
# Basic settings
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types
|
||||
application/javascript
|
||||
application/json
|
||||
application/xml
|
||||
text/css
|
||||
text/javascript
|
||||
text/plain
|
||||
text/xml;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
|
||||
# API proxy (in case of single-domain deployment)
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# WebSocket proxy
|
||||
location /ws {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket specific timeouts
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
}
|
||||
|
||||
# Error pages
|
||||
error_page 404 /index.html;
|
||||
}
|
||||
}
|
||||
1
frontend/node_modules/.bin/acorn
generated
vendored
1
frontend/node_modules/.bin/acorn
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../acorn/bin/acorn
|
||||
1
frontend/node_modules/.bin/autoprefixer
generated
vendored
1
frontend/node_modules/.bin/autoprefixer
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../autoprefixer/bin/autoprefixer
|
||||
1
frontend/node_modules/.bin/baseline-browser-mapping
generated
vendored
1
frontend/node_modules/.bin/baseline-browser-mapping
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../baseline-browser-mapping/dist/cli.js
|
||||
1
frontend/node_modules/.bin/browserslist
generated
vendored
1
frontend/node_modules/.bin/browserslist
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../browserslist/cli.js
|
||||
1
frontend/node_modules/.bin/cssesc
generated
vendored
1
frontend/node_modules/.bin/cssesc
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../cssesc/bin/cssesc
|
||||
1
frontend/node_modules/.bin/esbuild
generated
vendored
1
frontend/node_modules/.bin/esbuild
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../esbuild/bin/esbuild
|
||||
1
frontend/node_modules/.bin/eslint
generated
vendored
1
frontend/node_modules/.bin/eslint
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../eslint/bin/eslint.js
|
||||
1
frontend/node_modules/.bin/jiti
generated
vendored
1
frontend/node_modules/.bin/jiti
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../jiti/bin/jiti.js
|
||||
1
frontend/node_modules/.bin/js-yaml
generated
vendored
1
frontend/node_modules/.bin/js-yaml
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../js-yaml/bin/js-yaml.js
|
||||
1
frontend/node_modules/.bin/jsesc
generated
vendored
1
frontend/node_modules/.bin/jsesc
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../jsesc/bin/jsesc
|
||||
1
frontend/node_modules/.bin/json5
generated
vendored
1
frontend/node_modules/.bin/json5
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../json5/lib/cli.js
|
||||
1
frontend/node_modules/.bin/loose-envify
generated
vendored
1
frontend/node_modules/.bin/loose-envify
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../loose-envify/cli.js
|
||||
1
frontend/node_modules/.bin/nanoid
generated
vendored
1
frontend/node_modules/.bin/nanoid
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../nanoid/bin/nanoid.cjs
|
||||
1
frontend/node_modules/.bin/node-which
generated
vendored
1
frontend/node_modules/.bin/node-which
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../which/bin/node-which
|
||||
1
frontend/node_modules/.bin/parser
generated
vendored
1
frontend/node_modules/.bin/parser
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../@babel/parser/bin/babel-parser.js
|
||||
1
frontend/node_modules/.bin/resolve
generated
vendored
1
frontend/node_modules/.bin/resolve
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../resolve/bin/resolve
|
||||
1
frontend/node_modules/.bin/rimraf
generated
vendored
1
frontend/node_modules/.bin/rimraf
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../rimraf/bin.js
|
||||
1
frontend/node_modules/.bin/rollup
generated
vendored
1
frontend/node_modules/.bin/rollup
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../rollup/dist/bin/rollup
|
||||
1
frontend/node_modules/.bin/semver
generated
vendored
1
frontend/node_modules/.bin/semver
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../semver/bin/semver.js
|
||||
1
frontend/node_modules/.bin/sucrase
generated
vendored
1
frontend/node_modules/.bin/sucrase
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../sucrase/bin/sucrase
|
||||
1
frontend/node_modules/.bin/sucrase-node
generated
vendored
1
frontend/node_modules/.bin/sucrase-node
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../sucrase/bin/sucrase-node
|
||||
1
frontend/node_modules/.bin/tailwind
generated
vendored
1
frontend/node_modules/.bin/tailwind
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../tailwindcss/lib/cli.js
|
||||
1
frontend/node_modules/.bin/tailwindcss
generated
vendored
1
frontend/node_modules/.bin/tailwindcss
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../tailwindcss/lib/cli.js
|
||||
1
frontend/node_modules/.bin/tsc
generated
vendored
1
frontend/node_modules/.bin/tsc
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../typescript/bin/tsc
|
||||
1
frontend/node_modules/.bin/tsserver
generated
vendored
1
frontend/node_modules/.bin/tsserver
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../typescript/bin/tsserver
|
||||
1
frontend/node_modules/.bin/update-browserslist-db
generated
vendored
1
frontend/node_modules/.bin/update-browserslist-db
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../update-browserslist-db/cli.js
|
||||
1
frontend/node_modules/.bin/vite
generated
vendored
1
frontend/node_modules/.bin/vite
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../vite/bin/vite.js
|
||||
1
frontend/node_modules/.bin/yaml
generated
vendored
1
frontend/node_modules/.bin/yaml
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
../yaml/bin.mjs
|
||||
4875
frontend/node_modules/.package-lock.json
generated
vendored
4875
frontend/node_modules/.package-lock.json
generated
vendored
File diff suppressed because it is too large
Load diff
126
frontend/node_modules/.vite/deps/@azure_msal-browser.js
generated
vendored
126
frontend/node_modules/.vite/deps/@azure_msal-browser.js
generated
vendored
|
|
@ -1,126 +0,0 @@
|
|||
import {
|
||||
AccountEntity,
|
||||
ApiId,
|
||||
AuthError,
|
||||
AuthErrorCodes_exports,
|
||||
AuthErrorMessage,
|
||||
AuthenticationHeaderParser,
|
||||
AuthenticationScheme,
|
||||
AzureCloudInstance,
|
||||
BrowserAuthError,
|
||||
BrowserAuthErrorCodes_exports,
|
||||
BrowserAuthErrorMessage,
|
||||
BrowserCacheLocation,
|
||||
BrowserConfigurationAuthError,
|
||||
BrowserConfigurationAuthErrorCodes_exports,
|
||||
BrowserConfigurationAuthErrorMessage,
|
||||
BrowserPerformanceClient,
|
||||
BrowserStorage,
|
||||
BrowserUtils_exports,
|
||||
CacheLookupPolicy,
|
||||
ClientAuthError,
|
||||
ClientAuthErrorCodes_exports,
|
||||
ClientAuthErrorMessage,
|
||||
ClientConfigurationError,
|
||||
ClientConfigurationErrorCodes_exports,
|
||||
ClientConfigurationErrorMessage,
|
||||
DEFAULT_IFRAME_TIMEOUT_MS,
|
||||
EventHandler,
|
||||
EventMessageUtils,
|
||||
EventType,
|
||||
InteractionRequiredAuthError,
|
||||
InteractionRequiredAuthErrorCodes_exports,
|
||||
InteractionRequiredAuthErrorMessage,
|
||||
InteractionStatus,
|
||||
InteractionType,
|
||||
JsonWebTokenTypes,
|
||||
LocalStorage,
|
||||
LogLevel,
|
||||
Logger,
|
||||
MemoryStorage,
|
||||
NavigationClient,
|
||||
OIDC_DEFAULT_SCOPES,
|
||||
PerformanceEvents,
|
||||
PromptValue,
|
||||
ProtocolMode,
|
||||
PublicClientApplication,
|
||||
PublicClientNext,
|
||||
ServerError,
|
||||
ServerResponseType,
|
||||
SessionStorage,
|
||||
SignedHttpRequest,
|
||||
StringUtils,
|
||||
StubPerformanceClient,
|
||||
UrlString,
|
||||
WrapperSKU,
|
||||
createNestablePublicClientApplication,
|
||||
createStandardPublicClientApplication,
|
||||
stubbedPublicClientApplication,
|
||||
version
|
||||
} from "./chunk-2JU2OHEU.js";
|
||||
import {
|
||||
BrowserPerformanceMeasurement
|
||||
} from "./chunk-KV4DOQ5A.js";
|
||||
import "./chunk-4MBMRILA.js";
|
||||
export {
|
||||
AccountEntity,
|
||||
ApiId,
|
||||
AuthError,
|
||||
AuthErrorCodes_exports as AuthErrorCodes,
|
||||
AuthErrorMessage,
|
||||
AuthenticationHeaderParser,
|
||||
AuthenticationScheme,
|
||||
AzureCloudInstance,
|
||||
BrowserAuthError,
|
||||
BrowserAuthErrorCodes_exports as BrowserAuthErrorCodes,
|
||||
BrowserAuthErrorMessage,
|
||||
BrowserCacheLocation,
|
||||
BrowserConfigurationAuthError,
|
||||
BrowserConfigurationAuthErrorCodes_exports as BrowserConfigurationAuthErrorCodes,
|
||||
BrowserConfigurationAuthErrorMessage,
|
||||
BrowserPerformanceClient,
|
||||
BrowserPerformanceMeasurement,
|
||||
BrowserStorage,
|
||||
BrowserUtils_exports as BrowserUtils,
|
||||
CacheLookupPolicy,
|
||||
ClientAuthError,
|
||||
ClientAuthErrorCodes_exports as ClientAuthErrorCodes,
|
||||
ClientAuthErrorMessage,
|
||||
ClientConfigurationError,
|
||||
ClientConfigurationErrorCodes_exports as ClientConfigurationErrorCodes,
|
||||
ClientConfigurationErrorMessage,
|
||||
DEFAULT_IFRAME_TIMEOUT_MS,
|
||||
EventHandler,
|
||||
EventMessageUtils,
|
||||
EventType,
|
||||
InteractionRequiredAuthError,
|
||||
InteractionRequiredAuthErrorCodes_exports as InteractionRequiredAuthErrorCodes,
|
||||
InteractionRequiredAuthErrorMessage,
|
||||
InteractionStatus,
|
||||
InteractionType,
|
||||
JsonWebTokenTypes,
|
||||
LocalStorage,
|
||||
LogLevel,
|
||||
Logger,
|
||||
MemoryStorage,
|
||||
NavigationClient,
|
||||
OIDC_DEFAULT_SCOPES,
|
||||
PerformanceEvents,
|
||||
PromptValue,
|
||||
ProtocolMode,
|
||||
PublicClientApplication,
|
||||
PublicClientNext,
|
||||
ServerError,
|
||||
ServerResponseType,
|
||||
SessionStorage,
|
||||
SignedHttpRequest,
|
||||
StringUtils,
|
||||
StubPerformanceClient,
|
||||
UrlString,
|
||||
WrapperSKU,
|
||||
createNestablePublicClientApplication,
|
||||
createStandardPublicClientApplication,
|
||||
stubbedPublicClientApplication,
|
||||
version
|
||||
};
|
||||
//# sourceMappingURL=@azure_msal-browser.js.map
|
||||
7
frontend/node_modules/.vite/deps/@azure_msal-browser.js.map
generated
vendored
7
frontend/node_modules/.vite/deps/@azure_msal-browser.js.map
generated
vendored
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
549
frontend/node_modules/.vite/deps/@azure_msal-react.js
generated
vendored
549
frontend/node_modules/.vite/deps/@azure_msal-react.js
generated
vendored
|
|
@ -1,549 +0,0 @@
|
|||
import {
|
||||
require_react
|
||||
} from "./chunk-3TFVT2CW.js";
|
||||
import {
|
||||
AccountEntity,
|
||||
AuthError,
|
||||
EventMessageUtils,
|
||||
EventType,
|
||||
InteractionRequiredAuthError,
|
||||
InteractionStatus,
|
||||
InteractionType,
|
||||
Logger,
|
||||
OIDC_DEFAULT_SCOPES,
|
||||
WrapperSKU,
|
||||
stubbedPublicClientApplication
|
||||
} from "./chunk-2JU2OHEU.js";
|
||||
import "./chunk-KV4DOQ5A.js";
|
||||
import {
|
||||
__toESM
|
||||
} from "./chunk-4MBMRILA.js";
|
||||
|
||||
// node_modules/@azure/msal-react/dist/MsalContext.js
|
||||
var React = __toESM(require_react(), 1);
|
||||
var defaultMsalContext = {
|
||||
instance: stubbedPublicClientApplication,
|
||||
inProgress: InteractionStatus.None,
|
||||
accounts: [],
|
||||
logger: new Logger({})
|
||||
};
|
||||
var MsalContext = React.createContext(defaultMsalContext);
|
||||
var MsalConsumer = MsalContext.Consumer;
|
||||
|
||||
// node_modules/@azure/msal-react/dist/MsalProvider.js
|
||||
var import_react = __toESM(require_react(), 1);
|
||||
|
||||
// node_modules/@azure/msal-react/dist/utils/utilities.js
|
||||
function getChildrenOrFunction(children, args) {
|
||||
if (typeof children === "function") {
|
||||
return children(args);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
function accountArraysAreEqual(arrayA, arrayB) {
|
||||
if (arrayA.length !== arrayB.length) {
|
||||
return false;
|
||||
}
|
||||
const comparisonArray = [...arrayB];
|
||||
return arrayA.every((elementA) => {
|
||||
const elementB = comparisonArray.shift();
|
||||
if (!elementA || !elementB) {
|
||||
return false;
|
||||
}
|
||||
return elementA.homeAccountId === elementB.homeAccountId && elementA.localAccountId === elementB.localAccountId && elementA.username === elementB.username;
|
||||
});
|
||||
}
|
||||
function getAccountByIdentifiers(allAccounts, accountIdentifiers) {
|
||||
if (allAccounts.length > 0 && (accountIdentifiers.homeAccountId || accountIdentifiers.localAccountId || accountIdentifiers.username)) {
|
||||
const matchedAccounts = allAccounts.filter((accountObj) => {
|
||||
if (accountIdentifiers.username && accountIdentifiers.username.toLowerCase() !== accountObj.username.toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
if (accountIdentifiers.homeAccountId && accountIdentifiers.homeAccountId.toLowerCase() !== accountObj.homeAccountId.toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
if (accountIdentifiers.localAccountId && accountIdentifiers.localAccountId.toLowerCase() !== accountObj.localAccountId.toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return matchedAccounts[0] || null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// node_modules/@azure/msal-react/dist/packageMetadata.js
|
||||
var name = "@azure/msal-react";
|
||||
var version = "2.2.0";
|
||||
|
||||
// node_modules/@azure/msal-react/dist/MsalProvider.js
|
||||
var MsalProviderActionType = {
|
||||
UNBLOCK_INPROGRESS: "UNBLOCK_INPROGRESS",
|
||||
EVENT: "EVENT"
|
||||
};
|
||||
var reducer = (previousState, action) => {
|
||||
const { type, payload } = action;
|
||||
let newInProgress = previousState.inProgress;
|
||||
switch (type) {
|
||||
case MsalProviderActionType.UNBLOCK_INPROGRESS:
|
||||
if (previousState.inProgress === InteractionStatus.Startup) {
|
||||
newInProgress = InteractionStatus.None;
|
||||
payload.logger.info("MsalProvider - handleRedirectPromise resolved, setting inProgress to 'none'");
|
||||
}
|
||||
break;
|
||||
case MsalProviderActionType.EVENT:
|
||||
const message = payload.message;
|
||||
const status = EventMessageUtils.getInteractionStatusFromEvent(message, previousState.inProgress);
|
||||
if (status) {
|
||||
payload.logger.info(`MsalProvider - ${message.eventType} results in setting inProgress from ${previousState.inProgress} to ${status}`);
|
||||
newInProgress = status;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown action type: ${type}`);
|
||||
}
|
||||
const currentAccounts = payload.instance.getAllAccounts();
|
||||
if (newInProgress !== previousState.inProgress && !accountArraysAreEqual(currentAccounts, previousState.accounts)) {
|
||||
return {
|
||||
...previousState,
|
||||
inProgress: newInProgress,
|
||||
accounts: currentAccounts
|
||||
};
|
||||
} else if (newInProgress !== previousState.inProgress) {
|
||||
return {
|
||||
...previousState,
|
||||
inProgress: newInProgress
|
||||
};
|
||||
} else if (!accountArraysAreEqual(currentAccounts, previousState.accounts)) {
|
||||
return {
|
||||
...previousState,
|
||||
accounts: currentAccounts
|
||||
};
|
||||
} else {
|
||||
return previousState;
|
||||
}
|
||||
};
|
||||
function MsalProvider({ instance, children }) {
|
||||
(0, import_react.useEffect)(() => {
|
||||
instance.initializeWrapperLibrary(WrapperSKU.React, version);
|
||||
}, [instance]);
|
||||
const logger = (0, import_react.useMemo)(() => {
|
||||
return instance.getLogger().clone(name, version);
|
||||
}, [instance]);
|
||||
const [state, updateState] = (0, import_react.useReducer)(reducer, void 0, () => {
|
||||
return {
|
||||
inProgress: InteractionStatus.Startup,
|
||||
accounts: instance.getAllAccounts()
|
||||
};
|
||||
});
|
||||
(0, import_react.useEffect)(() => {
|
||||
const callbackId = instance.addEventCallback((message) => {
|
||||
updateState({
|
||||
payload: {
|
||||
instance,
|
||||
logger,
|
||||
message
|
||||
},
|
||||
type: MsalProviderActionType.EVENT
|
||||
});
|
||||
});
|
||||
logger.verbose(`MsalProvider - Registered event callback with id: ${callbackId}`);
|
||||
instance.initialize().then(() => {
|
||||
instance.handleRedirectPromise().catch(() => {
|
||||
return;
|
||||
}).finally(() => {
|
||||
updateState({
|
||||
payload: {
|
||||
instance,
|
||||
logger
|
||||
},
|
||||
type: MsalProviderActionType.UNBLOCK_INPROGRESS
|
||||
});
|
||||
});
|
||||
}).catch(() => {
|
||||
return;
|
||||
});
|
||||
return () => {
|
||||
if (callbackId) {
|
||||
logger.verbose(`MsalProvider - Removing event callback ${callbackId}`);
|
||||
instance.removeEventCallback(callbackId);
|
||||
}
|
||||
};
|
||||
}, [instance, logger]);
|
||||
const contextValue = {
|
||||
instance,
|
||||
inProgress: state.inProgress,
|
||||
accounts: state.accounts,
|
||||
logger
|
||||
};
|
||||
return import_react.default.createElement(MsalContext.Provider, { value: contextValue }, children);
|
||||
}
|
||||
|
||||
// node_modules/@azure/msal-react/dist/components/AuthenticatedTemplate.js
|
||||
var import_react4 = __toESM(require_react(), 1);
|
||||
|
||||
// node_modules/@azure/msal-react/dist/hooks/useMsal.js
|
||||
var import_react2 = __toESM(require_react(), 1);
|
||||
var useMsal = () => (0, import_react2.useContext)(MsalContext);
|
||||
|
||||
// node_modules/@azure/msal-react/dist/hooks/useIsAuthenticated.js
|
||||
var import_react3 = __toESM(require_react(), 1);
|
||||
function isAuthenticated(allAccounts, matchAccount) {
|
||||
if (matchAccount && (matchAccount.username || matchAccount.homeAccountId || matchAccount.localAccountId)) {
|
||||
return !!getAccountByIdentifiers(allAccounts, matchAccount);
|
||||
}
|
||||
return allAccounts.length > 0;
|
||||
}
|
||||
function useIsAuthenticated(matchAccount) {
|
||||
const { accounts: allAccounts, inProgress } = useMsal();
|
||||
const isUserAuthenticated = (0, import_react3.useMemo)(() => {
|
||||
if (inProgress === InteractionStatus.Startup) {
|
||||
return false;
|
||||
}
|
||||
return isAuthenticated(allAccounts, matchAccount);
|
||||
}, [allAccounts, inProgress, matchAccount]);
|
||||
return isUserAuthenticated;
|
||||
}
|
||||
|
||||
// node_modules/@azure/msal-react/dist/components/AuthenticatedTemplate.js
|
||||
function AuthenticatedTemplate({ username, homeAccountId, localAccountId, children }) {
|
||||
const context = useMsal();
|
||||
const accountIdentifier = (0, import_react4.useMemo)(() => {
|
||||
return {
|
||||
username,
|
||||
homeAccountId,
|
||||
localAccountId
|
||||
};
|
||||
}, [username, homeAccountId, localAccountId]);
|
||||
const isAuthenticated2 = useIsAuthenticated(accountIdentifier);
|
||||
if (isAuthenticated2 && context.inProgress !== InteractionStatus.Startup) {
|
||||
return import_react4.default.createElement(import_react4.default.Fragment, null, getChildrenOrFunction(children, context));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// node_modules/@azure/msal-react/dist/components/UnauthenticatedTemplate.js
|
||||
var import_react5 = __toESM(require_react(), 1);
|
||||
function UnauthenticatedTemplate({ username, homeAccountId, localAccountId, children }) {
|
||||
const context = useMsal();
|
||||
const accountIdentifier = (0, import_react5.useMemo)(() => {
|
||||
return {
|
||||
username,
|
||||
homeAccountId,
|
||||
localAccountId
|
||||
};
|
||||
}, [username, homeAccountId, localAccountId]);
|
||||
const isAuthenticated2 = useIsAuthenticated(accountIdentifier);
|
||||
if (!isAuthenticated2 && context.inProgress !== InteractionStatus.Startup && context.inProgress !== InteractionStatus.HandleRedirect) {
|
||||
return import_react5.default.createElement(import_react5.default.Fragment, null, getChildrenOrFunction(children, context));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// node_modules/@azure/msal-react/dist/components/MsalAuthenticationTemplate.js
|
||||
var import_react8 = __toESM(require_react(), 1);
|
||||
|
||||
// node_modules/@azure/msal-react/dist/hooks/useMsalAuthentication.js
|
||||
var import_react7 = __toESM(require_react(), 1);
|
||||
|
||||
// node_modules/@azure/msal-react/dist/hooks/useAccount.js
|
||||
var import_react6 = __toESM(require_react(), 1);
|
||||
function getAccount(instance, accountIdentifiers) {
|
||||
if (!accountIdentifiers || !accountIdentifiers.homeAccountId && !accountIdentifiers.localAccountId && !accountIdentifiers.username) {
|
||||
return instance.getActiveAccount();
|
||||
}
|
||||
return getAccountByIdentifiers(instance.getAllAccounts(), accountIdentifiers);
|
||||
}
|
||||
function useAccount(accountIdentifiers) {
|
||||
const { instance, inProgress, logger } = useMsal();
|
||||
const [account, setAccount] = (0, import_react6.useState)(() => getAccount(instance, accountIdentifiers));
|
||||
(0, import_react6.useEffect)(() => {
|
||||
setAccount((currentAccount) => {
|
||||
const nextAccount = getAccount(instance, accountIdentifiers);
|
||||
if (!AccountEntity.accountInfoIsEqual(currentAccount, nextAccount, true)) {
|
||||
logger.info("useAccount - Updating account");
|
||||
return nextAccount;
|
||||
}
|
||||
return currentAccount;
|
||||
});
|
||||
}, [inProgress, accountIdentifiers, instance, logger]);
|
||||
return account;
|
||||
}
|
||||
|
||||
// node_modules/@azure/msal-react/dist/error/ReactAuthError.js
|
||||
var ReactAuthErrorMessage = {
|
||||
invalidInteractionType: {
|
||||
code: "invalid_interaction_type",
|
||||
desc: "The provided interaction type is invalid."
|
||||
},
|
||||
unableToFallbackToInteraction: {
|
||||
code: "unable_to_fallback_to_interaction",
|
||||
desc: "Interaction is required but another interaction is already in progress. Please try again when the current interaction is complete."
|
||||
}
|
||||
};
|
||||
var ReactAuthError = class _ReactAuthError extends AuthError {
|
||||
constructor(errorCode, errorMessage) {
|
||||
super(errorCode, errorMessage);
|
||||
Object.setPrototypeOf(this, _ReactAuthError.prototype);
|
||||
this.name = "ReactAuthError";
|
||||
}
|
||||
static createInvalidInteractionTypeError() {
|
||||
return new _ReactAuthError(ReactAuthErrorMessage.invalidInteractionType.code, ReactAuthErrorMessage.invalidInteractionType.desc);
|
||||
}
|
||||
static createUnableToFallbackToInteractionError() {
|
||||
return new _ReactAuthError(ReactAuthErrorMessage.unableToFallbackToInteraction.code, ReactAuthErrorMessage.unableToFallbackToInteraction.desc);
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/@azure/msal-react/dist/hooks/useMsalAuthentication.js
|
||||
function useMsalAuthentication(interactionType, authenticationRequest, accountIdentifiers) {
|
||||
const { instance, inProgress, logger } = useMsal();
|
||||
const isAuthenticated2 = useIsAuthenticated(accountIdentifiers);
|
||||
const account = useAccount(accountIdentifiers);
|
||||
const [[result, error], setResponse] = (0, import_react7.useState)([null, null]);
|
||||
const mounted = (0, import_react7.useRef)(true);
|
||||
(0, import_react7.useEffect)(() => {
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
const interactionInProgress = (0, import_react7.useRef)(inProgress !== InteractionStatus.None);
|
||||
(0, import_react7.useEffect)(() => {
|
||||
interactionInProgress.current = inProgress !== InteractionStatus.None;
|
||||
}, [inProgress]);
|
||||
const shouldAcquireToken = (0, import_react7.useRef)(true);
|
||||
(0, import_react7.useEffect)(() => {
|
||||
if (!!error) {
|
||||
shouldAcquireToken.current = false;
|
||||
return;
|
||||
}
|
||||
if (!!result) {
|
||||
shouldAcquireToken.current = false;
|
||||
return;
|
||||
}
|
||||
}, [error, result]);
|
||||
const login = (0, import_react7.useCallback)(async (callbackInteractionType, callbackRequest) => {
|
||||
const loginType = callbackInteractionType || interactionType;
|
||||
const loginRequest = callbackRequest || authenticationRequest;
|
||||
switch (loginType) {
|
||||
case InteractionType.Popup:
|
||||
logger.verbose("useMsalAuthentication - Calling loginPopup");
|
||||
return instance.loginPopup(loginRequest);
|
||||
case InteractionType.Redirect:
|
||||
logger.verbose("useMsalAuthentication - Calling loginRedirect");
|
||||
return instance.loginRedirect(loginRequest).then(null);
|
||||
case InteractionType.Silent:
|
||||
logger.verbose("useMsalAuthentication - Calling ssoSilent");
|
||||
return instance.ssoSilent(loginRequest);
|
||||
default:
|
||||
throw ReactAuthError.createInvalidInteractionTypeError();
|
||||
}
|
||||
}, [instance, interactionType, authenticationRequest, logger]);
|
||||
const acquireToken = (0, import_react7.useCallback)(async (callbackInteractionType, callbackRequest) => {
|
||||
const fallbackInteractionType = callbackInteractionType || interactionType;
|
||||
let tokenRequest;
|
||||
if (callbackRequest) {
|
||||
logger.trace("useMsalAuthentication - acquireToken - Using request provided in the callback");
|
||||
tokenRequest = {
|
||||
...callbackRequest
|
||||
};
|
||||
} else if (authenticationRequest) {
|
||||
logger.trace("useMsalAuthentication - acquireToken - Using request provided in the hook");
|
||||
tokenRequest = {
|
||||
...authenticationRequest,
|
||||
scopes: authenticationRequest.scopes || OIDC_DEFAULT_SCOPES
|
||||
};
|
||||
} else {
|
||||
logger.trace("useMsalAuthentication - acquireToken - No request object provided, using default request.");
|
||||
tokenRequest = {
|
||||
scopes: OIDC_DEFAULT_SCOPES
|
||||
};
|
||||
}
|
||||
if (!tokenRequest.account && account) {
|
||||
logger.trace("useMsalAuthentication - acquireToken - Attaching account to request");
|
||||
tokenRequest.account = account;
|
||||
}
|
||||
const getToken = async () => {
|
||||
logger.verbose("useMsalAuthentication - Calling acquireTokenSilent");
|
||||
return instance.acquireTokenSilent(tokenRequest).catch(async (e) => {
|
||||
if (e instanceof InteractionRequiredAuthError) {
|
||||
if (!interactionInProgress.current) {
|
||||
logger.error("useMsalAuthentication - Interaction required, falling back to interaction");
|
||||
return login(fallbackInteractionType, tokenRequest);
|
||||
} else {
|
||||
logger.error("useMsalAuthentication - Interaction required but is already in progress. Please try again, if needed, after interaction completes.");
|
||||
throw ReactAuthError.createUnableToFallbackToInteractionError();
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
return getToken().then((response) => {
|
||||
if (mounted.current) {
|
||||
setResponse([response, null]);
|
||||
}
|
||||
return response;
|
||||
}).catch((e) => {
|
||||
if (mounted.current) {
|
||||
setResponse([null, e]);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}, [
|
||||
instance,
|
||||
interactionType,
|
||||
authenticationRequest,
|
||||
logger,
|
||||
account,
|
||||
login
|
||||
]);
|
||||
(0, import_react7.useEffect)(() => {
|
||||
const callbackId = instance.addEventCallback((message) => {
|
||||
switch (message.eventType) {
|
||||
case EventType.LOGIN_SUCCESS:
|
||||
case EventType.SSO_SILENT_SUCCESS:
|
||||
if (message.payload) {
|
||||
setResponse([
|
||||
message.payload,
|
||||
null
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case EventType.LOGIN_FAILURE:
|
||||
case EventType.SSO_SILENT_FAILURE:
|
||||
if (message.error) {
|
||||
setResponse([null, message.error]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
logger.verbose(`useMsalAuthentication - Registered event callback with id: ${callbackId}`);
|
||||
return () => {
|
||||
if (callbackId) {
|
||||
logger.verbose(`useMsalAuthentication - Removing event callback ${callbackId}`);
|
||||
instance.removeEventCallback(callbackId);
|
||||
}
|
||||
};
|
||||
}, [instance, logger]);
|
||||
(0, import_react7.useEffect)(() => {
|
||||
if (shouldAcquireToken.current && inProgress === InteractionStatus.None) {
|
||||
shouldAcquireToken.current = false;
|
||||
if (!isAuthenticated2) {
|
||||
logger.info("useMsalAuthentication - No user is authenticated, attempting to login");
|
||||
login().catch(() => {
|
||||
return;
|
||||
});
|
||||
} else if (account) {
|
||||
logger.info("useMsalAuthentication - User is authenticated, attempting to acquire token");
|
||||
acquireToken().catch(() => {
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isAuthenticated2, account, inProgress, login, acquireToken, logger]);
|
||||
return {
|
||||
login,
|
||||
acquireToken,
|
||||
result,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
// node_modules/@azure/msal-react/dist/components/MsalAuthenticationTemplate.js
|
||||
function MsalAuthenticationTemplate({ interactionType, username, homeAccountId, localAccountId, authenticationRequest, loadingComponent: LoadingComponent, errorComponent: ErrorComponent, children }) {
|
||||
const accountIdentifier = (0, import_react8.useMemo)(() => {
|
||||
return {
|
||||
username,
|
||||
homeAccountId,
|
||||
localAccountId
|
||||
};
|
||||
}, [username, homeAccountId, localAccountId]);
|
||||
const context = useMsal();
|
||||
const msalAuthResult = useMsalAuthentication(interactionType, authenticationRequest, accountIdentifier);
|
||||
const isAuthenticated2 = useIsAuthenticated(accountIdentifier);
|
||||
if (msalAuthResult.error && context.inProgress === InteractionStatus.None) {
|
||||
if (!!ErrorComponent) {
|
||||
return import_react8.default.createElement(ErrorComponent, { ...msalAuthResult });
|
||||
}
|
||||
throw msalAuthResult.error;
|
||||
}
|
||||
if (isAuthenticated2) {
|
||||
return import_react8.default.createElement(import_react8.default.Fragment, null, getChildrenOrFunction(children, msalAuthResult));
|
||||
}
|
||||
if (!!LoadingComponent && context.inProgress !== InteractionStatus.None) {
|
||||
return import_react8.default.createElement(LoadingComponent, { ...context });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// node_modules/@azure/msal-react/dist/components/withMsal.js
|
||||
var import_react9 = __toESM(require_react(), 1);
|
||||
var withMsal = (Component) => {
|
||||
const ComponentWithMsal = (props) => {
|
||||
const msal = useMsal();
|
||||
return import_react9.default.createElement(Component, { ...props, msalContext: msal });
|
||||
};
|
||||
const componentName = Component.displayName || Component.name || "Component";
|
||||
ComponentWithMsal.displayName = `withMsal(${componentName})`;
|
||||
return ComponentWithMsal;
|
||||
};
|
||||
export {
|
||||
AuthenticatedTemplate,
|
||||
MsalAuthenticationTemplate,
|
||||
MsalConsumer,
|
||||
MsalContext,
|
||||
MsalProvider,
|
||||
UnauthenticatedTemplate,
|
||||
useAccount,
|
||||
useIsAuthenticated,
|
||||
useMsal,
|
||||
useMsalAuthentication,
|
||||
version,
|
||||
withMsal
|
||||
};
|
||||
/*! Bundled license information:
|
||||
|
||||
@azure/msal-react/dist/MsalContext.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/utils/utilities.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/packageMetadata.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/MsalProvider.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/hooks/useMsal.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/hooks/useIsAuthenticated.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/components/AuthenticatedTemplate.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/components/UnauthenticatedTemplate.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/hooks/useAccount.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/error/ReactAuthError.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/hooks/useMsalAuthentication.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/components/MsalAuthenticationTemplate.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/components/withMsal.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
|
||||
@azure/msal-react/dist/index.js:
|
||||
(*! @azure/msal-react v2.2.0 2024-11-05 *)
|
||||
*/
|
||||
//# sourceMappingURL=@azure_msal-react.js.map
|
||||
7
frontend/node_modules/.vite/deps/@azure_msal-react.js.map
generated
vendored
7
frontend/node_modules/.vite/deps/@azure_msal-react.js.map
generated
vendored
File diff suppressed because one or more lines are too long
3591
frontend/node_modules/.vite/deps/@tanstack_react-query.js
generated
vendored
3591
frontend/node_modules/.vite/deps/@tanstack_react-query.js
generated
vendored
File diff suppressed because it is too large
Load diff
7
frontend/node_modules/.vite/deps/@tanstack_react-query.js.map
generated
vendored
7
frontend/node_modules/.vite/deps/@tanstack_react-query.js.map
generated
vendored
File diff suppressed because one or more lines are too long
8
frontend/node_modules/.vite/deps/BrowserPerformanceMeasurement-V7WDFHB7.js
generated
vendored
8
frontend/node_modules/.vite/deps/BrowserPerformanceMeasurement-V7WDFHB7.js
generated
vendored
|
|
@ -1,8 +0,0 @@
|
|||
import {
|
||||
BrowserPerformanceMeasurement
|
||||
} from "./chunk-KV4DOQ5A.js";
|
||||
import "./chunk-4MBMRILA.js";
|
||||
export {
|
||||
BrowserPerformanceMeasurement
|
||||
};
|
||||
//# sourceMappingURL=BrowserPerformanceMeasurement-V7WDFHB7.js.map
|
||||
7
frontend/node_modules/.vite/deps/BrowserPerformanceMeasurement-V7WDFHB7.js.map
generated
vendored
7
frontend/node_modules/.vite/deps/BrowserPerformanceMeasurement-V7WDFHB7.js.map
generated
vendored
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
109
frontend/node_modules/.vite/deps/_metadata.json
generated
vendored
109
frontend/node_modules/.vite/deps/_metadata.json
generated
vendored
|
|
@ -1,109 +0,0 @@
|
|||
{
|
||||
"hash": "74af12be",
|
||||
"configHash": "1c89d050",
|
||||
"lockfileHash": "b17ee09f",
|
||||
"browserHash": "e36fcd51",
|
||||
"optimized": {
|
||||
"react": {
|
||||
"src": "../../react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "4dce4127",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dom": {
|
||||
"src": "../../react-dom/index.js",
|
||||
"file": "react-dom.js",
|
||||
"fileHash": "085f5d08",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "27d9c06d",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "e65f4c52",
|
||||
"needsInterop": true
|
||||
},
|
||||
"@azure/msal-browser": {
|
||||
"src": "../../@azure/msal-browser/dist/index.mjs",
|
||||
"file": "@azure_msal-browser.js",
|
||||
"fileHash": "403472ec",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@azure/msal-react": {
|
||||
"src": "../../@azure/msal-react/dist/index.js",
|
||||
"file": "@azure_msal-react.js",
|
||||
"fileHash": "e8c854eb",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@tanstack/react-query": {
|
||||
"src": "../../@tanstack/react-query/build/modern/index.js",
|
||||
"file": "@tanstack_react-query.js",
|
||||
"fileHash": "1c1f3418",
|
||||
"needsInterop": false
|
||||
},
|
||||
"axios": {
|
||||
"src": "../../axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "feeb72e3",
|
||||
"needsInterop": false
|
||||
},
|
||||
"clsx": {
|
||||
"src": "../../clsx/dist/clsx.mjs",
|
||||
"file": "clsx.js",
|
||||
"fileHash": "c51ccc39",
|
||||
"needsInterop": false
|
||||
},
|
||||
"lucide-react": {
|
||||
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
||||
"file": "lucide-react.js",
|
||||
"fileHash": "3cf919ed",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "65ff411a",
|
||||
"needsInterop": true
|
||||
},
|
||||
"zustand": {
|
||||
"src": "../../zustand/esm/index.mjs",
|
||||
"file": "zustand.js",
|
||||
"fileHash": "eee18c92",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand/middleware": {
|
||||
"src": "../../zustand/esm/middleware.mjs",
|
||||
"file": "zustand_middleware.js",
|
||||
"fileHash": "137845f9",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"BrowserPerformanceMeasurement-V7WDFHB7": {
|
||||
"file": "BrowserPerformanceMeasurement-V7WDFHB7.js"
|
||||
},
|
||||
"chunk-WERSD76P": {
|
||||
"file": "chunk-WERSD76P.js"
|
||||
},
|
||||
"chunk-S77I6LSE": {
|
||||
"file": "chunk-S77I6LSE.js"
|
||||
},
|
||||
"chunk-3TFVT2CW": {
|
||||
"file": "chunk-3TFVT2CW.js"
|
||||
},
|
||||
"chunk-2JU2OHEU": {
|
||||
"file": "chunk-2JU2OHEU.js"
|
||||
},
|
||||
"chunk-KV4DOQ5A": {
|
||||
"file": "chunk-KV4DOQ5A.js"
|
||||
},
|
||||
"chunk-4MBMRILA": {
|
||||
"file": "chunk-4MBMRILA.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
2601
frontend/node_modules/.vite/deps/axios.js
generated
vendored
2601
frontend/node_modules/.vite/deps/axios.js
generated
vendored
File diff suppressed because it is too large
Load diff
7
frontend/node_modules/.vite/deps/axios.js.map
generated
vendored
7
frontend/node_modules/.vite/deps/axios.js.map
generated
vendored
File diff suppressed because one or more lines are too long
17142
frontend/node_modules/.vite/deps/chunk-2JU2OHEU.js
generated
vendored
17142
frontend/node_modules/.vite/deps/chunk-2JU2OHEU.js
generated
vendored
File diff suppressed because it is too large
Load diff
7
frontend/node_modules/.vite/deps/chunk-2JU2OHEU.js.map
generated
vendored
7
frontend/node_modules/.vite/deps/chunk-2JU2OHEU.js.map
generated
vendored
File diff suppressed because one or more lines are too long
1906
frontend/node_modules/.vite/deps/chunk-3TFVT2CW.js
generated
vendored
1906
frontend/node_modules/.vite/deps/chunk-3TFVT2CW.js
generated
vendored
File diff suppressed because it is too large
Load diff
7
frontend/node_modules/.vite/deps/chunk-3TFVT2CW.js.map
generated
vendored
7
frontend/node_modules/.vite/deps/chunk-3TFVT2CW.js.map
generated
vendored
File diff suppressed because one or more lines are too long
57
frontend/node_modules/.vite/deps/chunk-4MBMRILA.js
generated
vendored
57
frontend/node_modules/.vite/deps/chunk-4MBMRILA.js
generated
vendored
|
|
@ -1,57 +0,0 @@
|
|||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __typeError = (msg) => {
|
||||
throw TypeError(msg);
|
||||
};
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
||||
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
||||
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
||||
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
||||
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
||||
var __privateWrapper = (obj, member, setter, getter) => ({
|
||||
set _(value) {
|
||||
__privateSet(obj, member, value, setter);
|
||||
},
|
||||
get _() {
|
||||
return __privateGet(obj, member, getter);
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
__commonJS,
|
||||
__export,
|
||||
__toESM,
|
||||
__privateGet,
|
||||
__privateAdd,
|
||||
__privateSet,
|
||||
__privateMethod,
|
||||
__privateWrapper
|
||||
};
|
||||
//# sourceMappingURL=chunk-4MBMRILA.js.map
|
||||
7
frontend/node_modules/.vite/deps/chunk-4MBMRILA.js.map
generated
vendored
7
frontend/node_modules/.vite/deps/chunk-4MBMRILA.js.map
generated
vendored
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
85
frontend/node_modules/.vite/deps/chunk-KV4DOQ5A.js
generated
vendored
85
frontend/node_modules/.vite/deps/chunk-KV4DOQ5A.js
generated
vendored
|
|
@ -1,85 +0,0 @@
|
|||
// node_modules/@azure/msal-browser/dist/telemetry/BrowserPerformanceMeasurement.mjs
|
||||
var BrowserPerformanceMeasurement = class _BrowserPerformanceMeasurement {
|
||||
constructor(name, correlationId) {
|
||||
this.correlationId = correlationId;
|
||||
this.measureName = _BrowserPerformanceMeasurement.makeMeasureName(name, correlationId);
|
||||
this.startMark = _BrowserPerformanceMeasurement.makeStartMark(name, correlationId);
|
||||
this.endMark = _BrowserPerformanceMeasurement.makeEndMark(name, correlationId);
|
||||
}
|
||||
static makeMeasureName(name, correlationId) {
|
||||
return `msal.measure.${name}.${correlationId}`;
|
||||
}
|
||||
static makeStartMark(name, correlationId) {
|
||||
return `msal.start.${name}.${correlationId}`;
|
||||
}
|
||||
static makeEndMark(name, correlationId) {
|
||||
return `msal.end.${name}.${correlationId}`;
|
||||
}
|
||||
static supportsBrowserPerformance() {
|
||||
return typeof window !== "undefined" && typeof window.performance !== "undefined" && typeof window.performance.mark === "function" && typeof window.performance.measure === "function" && typeof window.performance.clearMarks === "function" && typeof window.performance.clearMeasures === "function" && typeof window.performance.getEntriesByName === "function";
|
||||
}
|
||||
/**
|
||||
* Flush browser marks and measurements.
|
||||
* @param {string} correlationId
|
||||
* @param {SubMeasurement} measurements
|
||||
*/
|
||||
static flushMeasurements(correlationId, measurements) {
|
||||
if (_BrowserPerformanceMeasurement.supportsBrowserPerformance()) {
|
||||
try {
|
||||
measurements.forEach((measurement) => {
|
||||
const measureName = _BrowserPerformanceMeasurement.makeMeasureName(measurement.name, correlationId);
|
||||
const entriesForMeasurement = window.performance.getEntriesByName(measureName, "measure");
|
||||
if (entriesForMeasurement.length > 0) {
|
||||
window.performance.clearMeasures(measureName);
|
||||
window.performance.clearMarks(_BrowserPerformanceMeasurement.makeStartMark(measureName, correlationId));
|
||||
window.performance.clearMarks(_BrowserPerformanceMeasurement.makeEndMark(measureName, correlationId));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
startMeasurement() {
|
||||
if (_BrowserPerformanceMeasurement.supportsBrowserPerformance()) {
|
||||
try {
|
||||
window.performance.mark(this.startMark);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
endMeasurement() {
|
||||
if (_BrowserPerformanceMeasurement.supportsBrowserPerformance()) {
|
||||
try {
|
||||
window.performance.mark(this.endMark);
|
||||
window.performance.measure(this.measureName, this.startMark, this.endMark);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
flushMeasurement() {
|
||||
if (_BrowserPerformanceMeasurement.supportsBrowserPerformance()) {
|
||||
try {
|
||||
const entriesForMeasurement = window.performance.getEntriesByName(this.measureName, "measure");
|
||||
if (entriesForMeasurement.length > 0) {
|
||||
const durationMs = entriesForMeasurement[0].duration;
|
||||
window.performance.clearMeasures(this.measureName);
|
||||
window.performance.clearMarks(this.startMark);
|
||||
window.performance.clearMarks(this.endMark);
|
||||
return durationMs;
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
BrowserPerformanceMeasurement
|
||||
};
|
||||
/*! Bundled license information:
|
||||
|
||||
@azure/msal-browser/dist/telemetry/BrowserPerformanceMeasurement.mjs:
|
||||
(*! @azure/msal-browser v3.30.0 2025-08-05 *)
|
||||
*/
|
||||
//# sourceMappingURL=chunk-KV4DOQ5A.js.map
|
||||
7
frontend/node_modules/.vite/deps/chunk-KV4DOQ5A.js.map
generated
vendored
7
frontend/node_modules/.vite/deps/chunk-KV4DOQ5A.js.map
generated
vendored
File diff suppressed because one or more lines are too long
928
frontend/node_modules/.vite/deps/chunk-S77I6LSE.js
generated
vendored
928
frontend/node_modules/.vite/deps/chunk-S77I6LSE.js
generated
vendored
|
|
@ -1,928 +0,0 @@
|
|||
import {
|
||||
require_react
|
||||
} from "./chunk-3TFVT2CW.js";
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-4MBMRILA.js";
|
||||
|
||||
// node_modules/react/cjs/react-jsx-runtime.development.js
|
||||
var require_react_jsx_runtime_development = __commonJS({
|
||||
"node_modules/react/cjs/react-jsx-runtime.development.js"(exports) {
|
||||
"use strict";
|
||||
if (true) {
|
||||
(function() {
|
||||
"use strict";
|
||||
var React = require_react();
|
||||
var REACT_ELEMENT_TYPE = Symbol.for("react.element");
|
||||
var REACT_PORTAL_TYPE = Symbol.for("react.portal");
|
||||
var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
|
||||
var REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode");
|
||||
var REACT_PROFILER_TYPE = Symbol.for("react.profiler");
|
||||
var REACT_PROVIDER_TYPE = Symbol.for("react.provider");
|
||||
var REACT_CONTEXT_TYPE = Symbol.for("react.context");
|
||||
var REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
|
||||
var REACT_SUSPENSE_TYPE = Symbol.for("react.suspense");
|
||||
var REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list");
|
||||
var REACT_MEMO_TYPE = Symbol.for("react.memo");
|
||||
var REACT_LAZY_TYPE = Symbol.for("react.lazy");
|
||||
var REACT_OFFSCREEN_TYPE = Symbol.for("react.offscreen");
|
||||
var MAYBE_ITERATOR_SYMBOL = Symbol.iterator;
|
||||
var FAUX_ITERATOR_SYMBOL = "@@iterator";
|
||||
function getIteratorFn(maybeIterable) {
|
||||
if (maybeIterable === null || typeof maybeIterable !== "object") {
|
||||
return null;
|
||||
}
|
||||
var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL];
|
||||
if (typeof maybeIterator === "function") {
|
||||
return maybeIterator;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
var ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
|
||||
function error(format) {
|
||||
{
|
||||
{
|
||||
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
||||
args[_key2 - 1] = arguments[_key2];
|
||||
}
|
||||
printWarning("error", format, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
function printWarning(level, format, args) {
|
||||
{
|
||||
var ReactDebugCurrentFrame2 = ReactSharedInternals.ReactDebugCurrentFrame;
|
||||
var stack = ReactDebugCurrentFrame2.getStackAddendum();
|
||||
if (stack !== "") {
|
||||
format += "%s";
|
||||
args = args.concat([stack]);
|
||||
}
|
||||
var argsWithFormat = args.map(function(item) {
|
||||
return String(item);
|
||||
});
|
||||
argsWithFormat.unshift("Warning: " + format);
|
||||
Function.prototype.apply.call(console[level], console, argsWithFormat);
|
||||
}
|
||||
}
|
||||
var enableScopeAPI = false;
|
||||
var enableCacheElement = false;
|
||||
var enableTransitionTracing = false;
|
||||
var enableLegacyHidden = false;
|
||||
var enableDebugTracing = false;
|
||||
var REACT_MODULE_REFERENCE;
|
||||
{
|
||||
REACT_MODULE_REFERENCE = Symbol.for("react.module.reference");
|
||||
}
|
||||
function isValidElementType(type) {
|
||||
if (typeof type === "string" || typeof type === "function") {
|
||||
return true;
|
||||
}
|
||||
if (type === REACT_FRAGMENT_TYPE || type === REACT_PROFILER_TYPE || enableDebugTracing || type === REACT_STRICT_MODE_TYPE || type === REACT_SUSPENSE_TYPE || type === REACT_SUSPENSE_LIST_TYPE || enableLegacyHidden || type === REACT_OFFSCREEN_TYPE || enableScopeAPI || enableCacheElement || enableTransitionTracing) {
|
||||
return true;
|
||||
}
|
||||
if (typeof type === "object" && type !== null) {
|
||||
if (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || // This needs to include all possible module reference object
|
||||
// types supported by any Flight configuration anywhere since
|
||||
// we don't know which Flight build this will end up being used
|
||||
// with.
|
||||
type.$$typeof === REACT_MODULE_REFERENCE || type.getModuleId !== void 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function getWrappedName(outerType, innerType, wrapperName) {
|
||||
var displayName = outerType.displayName;
|
||||
if (displayName) {
|
||||
return displayName;
|
||||
}
|
||||
var functionName = innerType.displayName || innerType.name || "";
|
||||
return functionName !== "" ? wrapperName + "(" + functionName + ")" : wrapperName;
|
||||
}
|
||||
function getContextName(type) {
|
||||
return type.displayName || "Context";
|
||||
}
|
||||
function getComponentNameFromType(type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
{
|
||||
if (typeof type.tag === "number") {
|
||||
error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue.");
|
||||
}
|
||||
}
|
||||
if (typeof type === "function") {
|
||||
return type.displayName || type.name || null;
|
||||
}
|
||||
if (typeof type === "string") {
|
||||
return type;
|
||||
}
|
||||
switch (type) {
|
||||
case REACT_FRAGMENT_TYPE:
|
||||
return "Fragment";
|
||||
case REACT_PORTAL_TYPE:
|
||||
return "Portal";
|
||||
case REACT_PROFILER_TYPE:
|
||||
return "Profiler";
|
||||
case REACT_STRICT_MODE_TYPE:
|
||||
return "StrictMode";
|
||||
case REACT_SUSPENSE_TYPE:
|
||||
return "Suspense";
|
||||
case REACT_SUSPENSE_LIST_TYPE:
|
||||
return "SuspenseList";
|
||||
}
|
||||
if (typeof type === "object") {
|
||||
switch (type.$$typeof) {
|
||||
case REACT_CONTEXT_TYPE:
|
||||
var context = type;
|
||||
return getContextName(context) + ".Consumer";
|
||||
case REACT_PROVIDER_TYPE:
|
||||
var provider = type;
|
||||
return getContextName(provider._context) + ".Provider";
|
||||
case REACT_FORWARD_REF_TYPE:
|
||||
return getWrappedName(type, type.render, "ForwardRef");
|
||||
case REACT_MEMO_TYPE:
|
||||
var outerName = type.displayName || null;
|
||||
if (outerName !== null) {
|
||||
return outerName;
|
||||
}
|
||||
return getComponentNameFromType(type.type) || "Memo";
|
||||
case REACT_LAZY_TYPE: {
|
||||
var lazyComponent = type;
|
||||
var payload = lazyComponent._payload;
|
||||
var init = lazyComponent._init;
|
||||
try {
|
||||
return getComponentNameFromType(init(payload));
|
||||
} catch (x) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
var assign = Object.assign;
|
||||
var disabledDepth = 0;
|
||||
var prevLog;
|
||||
var prevInfo;
|
||||
var prevWarn;
|
||||
var prevError;
|
||||
var prevGroup;
|
||||
var prevGroupCollapsed;
|
||||
var prevGroupEnd;
|
||||
function disabledLog() {
|
||||
}
|
||||
disabledLog.__reactDisabledLog = true;
|
||||
function disableLogs() {
|
||||
{
|
||||
if (disabledDepth === 0) {
|
||||
prevLog = console.log;
|
||||
prevInfo = console.info;
|
||||
prevWarn = console.warn;
|
||||
prevError = console.error;
|
||||
prevGroup = console.group;
|
||||
prevGroupCollapsed = console.groupCollapsed;
|
||||
prevGroupEnd = console.groupEnd;
|
||||
var props = {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: disabledLog,
|
||||
writable: true
|
||||
};
|
||||
Object.defineProperties(console, {
|
||||
info: props,
|
||||
log: props,
|
||||
warn: props,
|
||||
error: props,
|
||||
group: props,
|
||||
groupCollapsed: props,
|
||||
groupEnd: props
|
||||
});
|
||||
}
|
||||
disabledDepth++;
|
||||
}
|
||||
}
|
||||
function reenableLogs() {
|
||||
{
|
||||
disabledDepth--;
|
||||
if (disabledDepth === 0) {
|
||||
var props = {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true
|
||||
};
|
||||
Object.defineProperties(console, {
|
||||
log: assign({}, props, {
|
||||
value: prevLog
|
||||
}),
|
||||
info: assign({}, props, {
|
||||
value: prevInfo
|
||||
}),
|
||||
warn: assign({}, props, {
|
||||
value: prevWarn
|
||||
}),
|
||||
error: assign({}, props, {
|
||||
value: prevError
|
||||
}),
|
||||
group: assign({}, props, {
|
||||
value: prevGroup
|
||||
}),
|
||||
groupCollapsed: assign({}, props, {
|
||||
value: prevGroupCollapsed
|
||||
}),
|
||||
groupEnd: assign({}, props, {
|
||||
value: prevGroupEnd
|
||||
})
|
||||
});
|
||||
}
|
||||
if (disabledDepth < 0) {
|
||||
error("disabledDepth fell below zero. This is a bug in React. Please file an issue.");
|
||||
}
|
||||
}
|
||||
}
|
||||
var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
|
||||
var prefix;
|
||||
function describeBuiltInComponentFrame(name, source, ownerFn) {
|
||||
{
|
||||
if (prefix === void 0) {
|
||||
try {
|
||||
throw Error();
|
||||
} catch (x) {
|
||||
var match = x.stack.trim().match(/\n( *(at )?)/);
|
||||
prefix = match && match[1] || "";
|
||||
}
|
||||
}
|
||||
return "\n" + prefix + name;
|
||||
}
|
||||
}
|
||||
var reentry = false;
|
||||
var componentFrameCache;
|
||||
{
|
||||
var PossiblyWeakMap = typeof WeakMap === "function" ? WeakMap : Map;
|
||||
componentFrameCache = new PossiblyWeakMap();
|
||||
}
|
||||
function describeNativeComponentFrame(fn, construct) {
|
||||
if (!fn || reentry) {
|
||||
return "";
|
||||
}
|
||||
{
|
||||
var frame = componentFrameCache.get(fn);
|
||||
if (frame !== void 0) {
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
var control;
|
||||
reentry = true;
|
||||
var previousPrepareStackTrace = Error.prepareStackTrace;
|
||||
Error.prepareStackTrace = void 0;
|
||||
var previousDispatcher;
|
||||
{
|
||||
previousDispatcher = ReactCurrentDispatcher.current;
|
||||
ReactCurrentDispatcher.current = null;
|
||||
disableLogs();
|
||||
}
|
||||
try {
|
||||
if (construct) {
|
||||
var Fake = function() {
|
||||
throw Error();
|
||||
};
|
||||
Object.defineProperty(Fake.prototype, "props", {
|
||||
set: function() {
|
||||
throw Error();
|
||||
}
|
||||
});
|
||||
if (typeof Reflect === "object" && Reflect.construct) {
|
||||
try {
|
||||
Reflect.construct(Fake, []);
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
Reflect.construct(fn, [], Fake);
|
||||
} else {
|
||||
try {
|
||||
Fake.call();
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
fn.call(Fake.prototype);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
throw Error();
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
fn();
|
||||
}
|
||||
} catch (sample) {
|
||||
if (sample && control && typeof sample.stack === "string") {
|
||||
var sampleLines = sample.stack.split("\n");
|
||||
var controlLines = control.stack.split("\n");
|
||||
var s = sampleLines.length - 1;
|
||||
var c = controlLines.length - 1;
|
||||
while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
|
||||
c--;
|
||||
}
|
||||
for (; s >= 1 && c >= 0; s--, c--) {
|
||||
if (sampleLines[s] !== controlLines[c]) {
|
||||
if (s !== 1 || c !== 1) {
|
||||
do {
|
||||
s--;
|
||||
c--;
|
||||
if (c < 0 || sampleLines[s] !== controlLines[c]) {
|
||||
var _frame = "\n" + sampleLines[s].replace(" at new ", " at ");
|
||||
if (fn.displayName && _frame.includes("<anonymous>")) {
|
||||
_frame = _frame.replace("<anonymous>", fn.displayName);
|
||||
}
|
||||
{
|
||||
if (typeof fn === "function") {
|
||||
componentFrameCache.set(fn, _frame);
|
||||
}
|
||||
}
|
||||
return _frame;
|
||||
}
|
||||
} while (s >= 1 && c >= 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reentry = false;
|
||||
{
|
||||
ReactCurrentDispatcher.current = previousDispatcher;
|
||||
reenableLogs();
|
||||
}
|
||||
Error.prepareStackTrace = previousPrepareStackTrace;
|
||||
}
|
||||
var name = fn ? fn.displayName || fn.name : "";
|
||||
var syntheticFrame = name ? describeBuiltInComponentFrame(name) : "";
|
||||
{
|
||||
if (typeof fn === "function") {
|
||||
componentFrameCache.set(fn, syntheticFrame);
|
||||
}
|
||||
}
|
||||
return syntheticFrame;
|
||||
}
|
||||
function describeFunctionComponentFrame(fn, source, ownerFn) {
|
||||
{
|
||||
return describeNativeComponentFrame(fn, false);
|
||||
}
|
||||
}
|
||||
function shouldConstruct(Component) {
|
||||
var prototype = Component.prototype;
|
||||
return !!(prototype && prototype.isReactComponent);
|
||||
}
|
||||
function describeUnknownElementTypeFrameInDEV(type, source, ownerFn) {
|
||||
if (type == null) {
|
||||
return "";
|
||||
}
|
||||
if (typeof type === "function") {
|
||||
{
|
||||
return describeNativeComponentFrame(type, shouldConstruct(type));
|
||||
}
|
||||
}
|
||||
if (typeof type === "string") {
|
||||
return describeBuiltInComponentFrame(type);
|
||||
}
|
||||
switch (type) {
|
||||
case REACT_SUSPENSE_TYPE:
|
||||
return describeBuiltInComponentFrame("Suspense");
|
||||
case REACT_SUSPENSE_LIST_TYPE:
|
||||
return describeBuiltInComponentFrame("SuspenseList");
|
||||
}
|
||||
if (typeof type === "object") {
|
||||
switch (type.$$typeof) {
|
||||
case REACT_FORWARD_REF_TYPE:
|
||||
return describeFunctionComponentFrame(type.render);
|
||||
case REACT_MEMO_TYPE:
|
||||
return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn);
|
||||
case REACT_LAZY_TYPE: {
|
||||
var lazyComponent = type;
|
||||
var payload = lazyComponent._payload;
|
||||
var init = lazyComponent._init;
|
||||
try {
|
||||
return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn);
|
||||
} catch (x) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
var loggedTypeFailures = {};
|
||||
var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
|
||||
function setCurrentlyValidatingElement(element) {
|
||||
{
|
||||
if (element) {
|
||||
var owner = element._owner;
|
||||
var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
|
||||
ReactDebugCurrentFrame.setExtraStackFrame(stack);
|
||||
} else {
|
||||
ReactDebugCurrentFrame.setExtraStackFrame(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
function checkPropTypes(typeSpecs, values, location, componentName, element) {
|
||||
{
|
||||
var has = Function.call.bind(hasOwnProperty);
|
||||
for (var typeSpecName in typeSpecs) {
|
||||
if (has(typeSpecs, typeSpecName)) {
|
||||
var error$1 = void 0;
|
||||
try {
|
||||
if (typeof typeSpecs[typeSpecName] !== "function") {
|
||||
var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
|
||||
err.name = "Invariant Violation";
|
||||
throw err;
|
||||
}
|
||||
error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
|
||||
} catch (ex) {
|
||||
error$1 = ex;
|
||||
}
|
||||
if (error$1 && !(error$1 instanceof Error)) {
|
||||
setCurrentlyValidatingElement(element);
|
||||
error("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1);
|
||||
setCurrentlyValidatingElement(null);
|
||||
}
|
||||
if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
|
||||
loggedTypeFailures[error$1.message] = true;
|
||||
setCurrentlyValidatingElement(element);
|
||||
error("Failed %s type: %s", location, error$1.message);
|
||||
setCurrentlyValidatingElement(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var isArrayImpl = Array.isArray;
|
||||
function isArray(a) {
|
||||
return isArrayImpl(a);
|
||||
}
|
||||
function typeName(value) {
|
||||
{
|
||||
var hasToStringTag = typeof Symbol === "function" && Symbol.toStringTag;
|
||||
var type = hasToStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
|
||||
return type;
|
||||
}
|
||||
}
|
||||
function willCoercionThrow(value) {
|
||||
{
|
||||
try {
|
||||
testStringCoercion(value);
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
function testStringCoercion(value) {
|
||||
return "" + value;
|
||||
}
|
||||
function checkKeyStringCoercion(value) {
|
||||
{
|
||||
if (willCoercionThrow(value)) {
|
||||
error("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.", typeName(value));
|
||||
return testStringCoercion(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
var ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
|
||||
var RESERVED_PROPS = {
|
||||
key: true,
|
||||
ref: true,
|
||||
__self: true,
|
||||
__source: true
|
||||
};
|
||||
var specialPropKeyWarningShown;
|
||||
var specialPropRefWarningShown;
|
||||
var didWarnAboutStringRefs;
|
||||
{
|
||||
didWarnAboutStringRefs = {};
|
||||
}
|
||||
function hasValidRef(config) {
|
||||
{
|
||||
if (hasOwnProperty.call(config, "ref")) {
|
||||
var getter = Object.getOwnPropertyDescriptor(config, "ref").get;
|
||||
if (getter && getter.isReactWarning) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return config.ref !== void 0;
|
||||
}
|
||||
function hasValidKey(config) {
|
||||
{
|
||||
if (hasOwnProperty.call(config, "key")) {
|
||||
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
|
||||
if (getter && getter.isReactWarning) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return config.key !== void 0;
|
||||
}
|
||||
function warnIfStringRefCannotBeAutoConverted(config, self) {
|
||||
{
|
||||
if (typeof config.ref === "string" && ReactCurrentOwner.current && self && ReactCurrentOwner.current.stateNode !== self) {
|
||||
var componentName = getComponentNameFromType(ReactCurrentOwner.current.type);
|
||||
if (!didWarnAboutStringRefs[componentName]) {
|
||||
error('Component "%s" contains the string ref "%s". Support for string refs will be removed in a future major release. This case cannot be automatically converted to an arrow function. We ask you to manually fix this case by using useRef() or createRef() instead. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref', getComponentNameFromType(ReactCurrentOwner.current.type), config.ref);
|
||||
didWarnAboutStringRefs[componentName] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function defineKeyPropWarningGetter(props, displayName) {
|
||||
{
|
||||
var warnAboutAccessingKey = function() {
|
||||
if (!specialPropKeyWarningShown) {
|
||||
specialPropKeyWarningShown = true;
|
||||
error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName);
|
||||
}
|
||||
};
|
||||
warnAboutAccessingKey.isReactWarning = true;
|
||||
Object.defineProperty(props, "key", {
|
||||
get: warnAboutAccessingKey,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
function defineRefPropWarningGetter(props, displayName) {
|
||||
{
|
||||
var warnAboutAccessingRef = function() {
|
||||
if (!specialPropRefWarningShown) {
|
||||
specialPropRefWarningShown = true;
|
||||
error("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName);
|
||||
}
|
||||
};
|
||||
warnAboutAccessingRef.isReactWarning = true;
|
||||
Object.defineProperty(props, "ref", {
|
||||
get: warnAboutAccessingRef,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
var ReactElement = function(type, key, ref, self, source, owner, props) {
|
||||
var element = {
|
||||
// This tag allows us to uniquely identify this as a React Element
|
||||
$$typeof: REACT_ELEMENT_TYPE,
|
||||
// Built-in properties that belong on the element
|
||||
type,
|
||||
key,
|
||||
ref,
|
||||
props,
|
||||
// Record the component responsible for creating this element.
|
||||
_owner: owner
|
||||
};
|
||||
{
|
||||
element._store = {};
|
||||
Object.defineProperty(element._store, "validated", {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: false
|
||||
});
|
||||
Object.defineProperty(element, "_self", {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: self
|
||||
});
|
||||
Object.defineProperty(element, "_source", {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: source
|
||||
});
|
||||
if (Object.freeze) {
|
||||
Object.freeze(element.props);
|
||||
Object.freeze(element);
|
||||
}
|
||||
}
|
||||
return element;
|
||||
};
|
||||
function jsxDEV(type, config, maybeKey, source, self) {
|
||||
{
|
||||
var propName;
|
||||
var props = {};
|
||||
var key = null;
|
||||
var ref = null;
|
||||
if (maybeKey !== void 0) {
|
||||
{
|
||||
checkKeyStringCoercion(maybeKey);
|
||||
}
|
||||
key = "" + maybeKey;
|
||||
}
|
||||
if (hasValidKey(config)) {
|
||||
{
|
||||
checkKeyStringCoercion(config.key);
|
||||
}
|
||||
key = "" + config.key;
|
||||
}
|
||||
if (hasValidRef(config)) {
|
||||
ref = config.ref;
|
||||
warnIfStringRefCannotBeAutoConverted(config, self);
|
||||
}
|
||||
for (propName in config) {
|
||||
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
|
||||
props[propName] = config[propName];
|
||||
}
|
||||
}
|
||||
if (type && type.defaultProps) {
|
||||
var defaultProps = type.defaultProps;
|
||||
for (propName in defaultProps) {
|
||||
if (props[propName] === void 0) {
|
||||
props[propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key || ref) {
|
||||
var displayName = typeof type === "function" ? type.displayName || type.name || "Unknown" : type;
|
||||
if (key) {
|
||||
defineKeyPropWarningGetter(props, displayName);
|
||||
}
|
||||
if (ref) {
|
||||
defineRefPropWarningGetter(props, displayName);
|
||||
}
|
||||
}
|
||||
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
|
||||
}
|
||||
}
|
||||
var ReactCurrentOwner$1 = ReactSharedInternals.ReactCurrentOwner;
|
||||
var ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame;
|
||||
function setCurrentlyValidatingElement$1(element) {
|
||||
{
|
||||
if (element) {
|
||||
var owner = element._owner;
|
||||
var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
|
||||
ReactDebugCurrentFrame$1.setExtraStackFrame(stack);
|
||||
} else {
|
||||
ReactDebugCurrentFrame$1.setExtraStackFrame(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
var propTypesMisspellWarningShown;
|
||||
{
|
||||
propTypesMisspellWarningShown = false;
|
||||
}
|
||||
function isValidElement(object) {
|
||||
{
|
||||
return typeof object === "object" && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
|
||||
}
|
||||
}
|
||||
function getDeclarationErrorAddendum() {
|
||||
{
|
||||
if (ReactCurrentOwner$1.current) {
|
||||
var name = getComponentNameFromType(ReactCurrentOwner$1.current.type);
|
||||
if (name) {
|
||||
return "\n\nCheck the render method of `" + name + "`.";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function getSourceInfoErrorAddendum(source) {
|
||||
{
|
||||
if (source !== void 0) {
|
||||
var fileName = source.fileName.replace(/^.*[\\\/]/, "");
|
||||
var lineNumber = source.lineNumber;
|
||||
return "\n\nCheck your code at " + fileName + ":" + lineNumber + ".";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
var ownerHasKeyUseWarning = {};
|
||||
function getCurrentComponentErrorInfo(parentType) {
|
||||
{
|
||||
var info = getDeclarationErrorAddendum();
|
||||
if (!info) {
|
||||
var parentName = typeof parentType === "string" ? parentType : parentType.displayName || parentType.name;
|
||||
if (parentName) {
|
||||
info = "\n\nCheck the top-level render call using <" + parentName + ">.";
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
}
|
||||
function validateExplicitKey(element, parentType) {
|
||||
{
|
||||
if (!element._store || element._store.validated || element.key != null) {
|
||||
return;
|
||||
}
|
||||
element._store.validated = true;
|
||||
var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType);
|
||||
if (ownerHasKeyUseWarning[currentComponentErrorInfo]) {
|
||||
return;
|
||||
}
|
||||
ownerHasKeyUseWarning[currentComponentErrorInfo] = true;
|
||||
var childOwner = "";
|
||||
if (element && element._owner && element._owner !== ReactCurrentOwner$1.current) {
|
||||
childOwner = " It was passed a child from " + getComponentNameFromType(element._owner.type) + ".";
|
||||
}
|
||||
setCurrentlyValidatingElement$1(element);
|
||||
error('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.', currentComponentErrorInfo, childOwner);
|
||||
setCurrentlyValidatingElement$1(null);
|
||||
}
|
||||
}
|
||||
function validateChildKeys(node, parentType) {
|
||||
{
|
||||
if (typeof node !== "object") {
|
||||
return;
|
||||
}
|
||||
if (isArray(node)) {
|
||||
for (var i = 0; i < node.length; i++) {
|
||||
var child = node[i];
|
||||
if (isValidElement(child)) {
|
||||
validateExplicitKey(child, parentType);
|
||||
}
|
||||
}
|
||||
} else if (isValidElement(node)) {
|
||||
if (node._store) {
|
||||
node._store.validated = true;
|
||||
}
|
||||
} else if (node) {
|
||||
var iteratorFn = getIteratorFn(node);
|
||||
if (typeof iteratorFn === "function") {
|
||||
if (iteratorFn !== node.entries) {
|
||||
var iterator = iteratorFn.call(node);
|
||||
var step;
|
||||
while (!(step = iterator.next()).done) {
|
||||
if (isValidElement(step.value)) {
|
||||
validateExplicitKey(step.value, parentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function validatePropTypes(element) {
|
||||
{
|
||||
var type = element.type;
|
||||
if (type === null || type === void 0 || typeof type === "string") {
|
||||
return;
|
||||
}
|
||||
var propTypes;
|
||||
if (typeof type === "function") {
|
||||
propTypes = type.propTypes;
|
||||
} else if (typeof type === "object" && (type.$$typeof === REACT_FORWARD_REF_TYPE || // Note: Memo only checks outer props here.
|
||||
// Inner props are checked in the reconciler.
|
||||
type.$$typeof === REACT_MEMO_TYPE)) {
|
||||
propTypes = type.propTypes;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (propTypes) {
|
||||
var name = getComponentNameFromType(type);
|
||||
checkPropTypes(propTypes, element.props, "prop", name, element);
|
||||
} else if (type.PropTypes !== void 0 && !propTypesMisspellWarningShown) {
|
||||
propTypesMisspellWarningShown = true;
|
||||
var _name = getComponentNameFromType(type);
|
||||
error("Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?", _name || "Unknown");
|
||||
}
|
||||
if (typeof type.getDefaultProps === "function" && !type.getDefaultProps.isReactClassApproved) {
|
||||
error("getDefaultProps is only used on classic React.createClass definitions. Use a static property named `defaultProps` instead.");
|
||||
}
|
||||
}
|
||||
}
|
||||
function validateFragmentProps(fragment) {
|
||||
{
|
||||
var keys = Object.keys(fragment.props);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
if (key !== "children" && key !== "key") {
|
||||
setCurrentlyValidatingElement$1(fragment);
|
||||
error("Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.", key);
|
||||
setCurrentlyValidatingElement$1(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fragment.ref !== null) {
|
||||
setCurrentlyValidatingElement$1(fragment);
|
||||
error("Invalid attribute `ref` supplied to `React.Fragment`.");
|
||||
setCurrentlyValidatingElement$1(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
var didWarnAboutKeySpread = {};
|
||||
function jsxWithValidation(type, props, key, isStaticChildren, source, self) {
|
||||
{
|
||||
var validType = isValidElementType(type);
|
||||
if (!validType) {
|
||||
var info = "";
|
||||
if (type === void 0 || typeof type === "object" && type !== null && Object.keys(type).length === 0) {
|
||||
info += " You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.";
|
||||
}
|
||||
var sourceInfo = getSourceInfoErrorAddendum(source);
|
||||
if (sourceInfo) {
|
||||
info += sourceInfo;
|
||||
} else {
|
||||
info += getDeclarationErrorAddendum();
|
||||
}
|
||||
var typeString;
|
||||
if (type === null) {
|
||||
typeString = "null";
|
||||
} else if (isArray(type)) {
|
||||
typeString = "array";
|
||||
} else if (type !== void 0 && type.$$typeof === REACT_ELEMENT_TYPE) {
|
||||
typeString = "<" + (getComponentNameFromType(type.type) || "Unknown") + " />";
|
||||
info = " Did you accidentally export a JSX literal instead of a component?";
|
||||
} else {
|
||||
typeString = typeof type;
|
||||
}
|
||||
error("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s", typeString, info);
|
||||
}
|
||||
var element = jsxDEV(type, props, key, source, self);
|
||||
if (element == null) {
|
||||
return element;
|
||||
}
|
||||
if (validType) {
|
||||
var children = props.children;
|
||||
if (children !== void 0) {
|
||||
if (isStaticChildren) {
|
||||
if (isArray(children)) {
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
validateChildKeys(children[i], type);
|
||||
}
|
||||
if (Object.freeze) {
|
||||
Object.freeze(children);
|
||||
}
|
||||
} else {
|
||||
error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");
|
||||
}
|
||||
} else {
|
||||
validateChildKeys(children, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
if (hasOwnProperty.call(props, "key")) {
|
||||
var componentName = getComponentNameFromType(type);
|
||||
var keys = Object.keys(props).filter(function(k) {
|
||||
return k !== "key";
|
||||
});
|
||||
var beforeExample = keys.length > 0 ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}";
|
||||
if (!didWarnAboutKeySpread[componentName + beforeExample]) {
|
||||
var afterExample = keys.length > 0 ? "{" + keys.join(": ..., ") + ": ...}" : "{}";
|
||||
error('A props object containing a "key" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />', beforeExample, componentName, afterExample, componentName);
|
||||
didWarnAboutKeySpread[componentName + beforeExample] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type === REACT_FRAGMENT_TYPE) {
|
||||
validateFragmentProps(element);
|
||||
} else {
|
||||
validatePropTypes(element);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
function jsxWithValidationStatic(type, props, key) {
|
||||
{
|
||||
return jsxWithValidation(type, props, key, true);
|
||||
}
|
||||
}
|
||||
function jsxWithValidationDynamic(type, props, key) {
|
||||
{
|
||||
return jsxWithValidation(type, props, key, false);
|
||||
}
|
||||
}
|
||||
var jsx = jsxWithValidationDynamic;
|
||||
var jsxs = jsxWithValidationStatic;
|
||||
exports.Fragment = REACT_FRAGMENT_TYPE;
|
||||
exports.jsx = jsx;
|
||||
exports.jsxs = jsxs;
|
||||
})();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/react/jsx-runtime.js
|
||||
var require_jsx_runtime = __commonJS({
|
||||
"node_modules/react/jsx-runtime.js"(exports, module) {
|
||||
if (false) {
|
||||
module.exports = null;
|
||||
} else {
|
||||
module.exports = require_react_jsx_runtime_development();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
require_jsx_runtime
|
||||
};
|
||||
/*! Bundled license information:
|
||||
|
||||
react/cjs/react-jsx-runtime.development.js:
|
||||
(**
|
||||
* @license React
|
||||
* react-jsx-runtime.development.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*)
|
||||
*/
|
||||
//# sourceMappingURL=chunk-S77I6LSE.js.map
|
||||
7
frontend/node_modules/.vite/deps/chunk-S77I6LSE.js.map
generated
vendored
7
frontend/node_modules/.vite/deps/chunk-S77I6LSE.js.map
generated
vendored
File diff suppressed because one or more lines are too long
21628
frontend/node_modules/.vite/deps/chunk-WERSD76P.js
generated
vendored
21628
frontend/node_modules/.vite/deps/chunk-WERSD76P.js
generated
vendored
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue