Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions chatbot-app/backend/app/config/ai_models.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"models": [
{
"id": "gpt4omni",
"name": "GPT-4 Omni",
"displayName": "GPT-4 Omni",
"provider": "azure_openai",
"description": "Advanced AI model for address standardization",
"enabled": true
}
],
"default_model": "gpt4omni"
}
155 changes: 145 additions & 10 deletions chatbot-app/backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,86 @@ def health_check():
'/api/process-addresses',
'/api/public/standardize',
'/api/coordinates',
'/api/countries'
'/api/countries',
'/api/models'
]
}), 200

# AI Model Management
def _load_ai_models():
"""Load AI models configuration from JSON file"""
try:
config_path = Path(__file__).parent / 'config' / 'ai_models.json'
with open(config_path, 'r') as f:
config = json.load(f)
return config
except Exception as e:
print(f"⚠️ Failed to load AI models configuration: {e}")
# Return default configuration
return {
'models': [
{
'id': 'gpt4omni',
'name': 'GPT-4 Omni',
'displayName': 'GPT-4 Omni',
'provider': 'azure_openai',
'description': 'Advanced AI model for address standardization',
'enabled': True
}
],
'default_model': 'gpt4omni'
}

def _validate_model(model_id: str) -> dict:
"""
Validate if the provided model ID is valid and enabled.
Returns dict with 'valid' (bool), 'error' (str), and 'model_config' (dict) keys.
"""
if not model_id:
return {'valid': False, 'error': 'Model ID is required', 'model_config': None}

config = _load_ai_models()
models = config.get('models', [])

# Find the model by ID
model_config = None
for model in models:
if model.get('id') == model_id:
model_config = model
break

if not model_config:
return {'valid': False, 'error': f'Invalid model ID: {model_id}', 'model_config': None}

if not model_config.get('enabled', False):
return {'valid': False, 'error': f'Model is disabled: {model_id}', 'model_config': None}

return {'valid': True, 'error': None, 'model_config': model_config}

@app.route('/api/models', methods=['GET'])
def get_ai_models():
"""Get list of available AI models (only returns display names and IDs, not internal config)"""
try:
config = _load_ai_models()
models = config.get('models', [])

# Filter enabled models and return only safe fields
available_models = []
for model in models:
if model.get('enabled', False):
available_models.append({
'id': model.get('id'),
'displayName': model.get('displayName'),
'description': model.get('description', '')
})

return jsonify({
'models': available_models,
'default_model': config.get('default_model', 'gpt4omni')
}), 200
except Exception as e:
return jsonify({'error': f'Failed to fetch models: {str(e)}'}), 500

# Basic request logging to help debug proxy path issues
@app.before_request
def log_request():
Expand Down Expand Up @@ -738,7 +814,7 @@ def upload_excel_redirect():
'debug_info': 'The proxy configuration may not be working correctly'
}), 404

def process_file_background(processing_id, filename):
def process_file_background(processing_id, filename, model_id=None):
"""Process the uploaded file in-process using CSVAddressProcessor for better progress feedback."""
try:
inbound_file = INBOUND_FOLDER / filename
Expand All @@ -748,7 +824,7 @@ def process_file_background(processing_id, filename):

_update_status(processing_id, status='processing', message='Initializing processor...', progress=20, log='Processor initialization')

processor = CSVAddressProcessor(base_directory=str(BASE_DIR))
processor = CSVAddressProcessor(base_directory=str(BASE_DIR), model=model_id)
_update_status(processing_id, message='Reading input file...', progress=35, log='Reading input file')
time.sleep(0.15)

Expand All @@ -765,7 +841,7 @@ def process_file_background(processing_id, filename):
except Exception as e:
_update_status(processing_id, status='error', message='Processing failed with error', progress=100, error=str(e), log=f'Exception: {e}')

def process_compare_background(processing_id, filename):
def process_compare_background(processing_id, filename, model_id=None):
"""Run batch compare across inbound via subprocess and detect produced outbound file."""
try:
inbound_file = INBOUND_FOLDER / filename
Expand All @@ -788,6 +864,11 @@ def process_compare_background(processing_id, filename):
'--compare-csv',
'--batch-size', '5'
]

# Add model parameter if provided
if model_id:
cmd.extend(['--model', model_id])

try:
recent_lines = []
child_env = os.environ.copy()
Expand Down Expand Up @@ -1053,6 +1134,20 @@ def upload_excel():
if not allowed_file(file.filename):
return jsonify({'error': 'Invalid file type. Please upload Excel (.xlsx, .xls) or CSV files.'}), 400

# Get and validate model parameter
model_id = request.form.get('model')
if not model_id:
# Use default model if not provided
config = _load_ai_models()
model_id = config.get('default_model', 'gpt4omni')

# Validate model
model_validation = _validate_model(model_id)
if not model_validation['valid']:
return jsonify({'error': model_validation['error']}), 400

model_config = model_validation['model_config']

# Generate secure filename with timestamp
filename = secure_filename(file.filename)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
Expand Down Expand Up @@ -1175,7 +1270,7 @@ def upload_excel():
# Start batch processing in background thread
thread = threading.Thread(
target=process_file_background,
args=(processing_id, unique_filename)
args=(processing_id, unique_filename, model_id)
)
thread.daemon = True
thread.start()
Expand All @@ -1185,7 +1280,8 @@ def upload_excel():
'message': f'File uploaded successfully! Processing started.',
'processing_id': processing_id,
'filename': unique_filename,
'file_info': file_info
'file_info': file_info,
'model': model_config.get('displayName')
}), 200

except Exception as e:
Expand All @@ -1203,6 +1299,20 @@ def upload_compare():
if not allowed_file(file.filename):
return jsonify({'error': 'Invalid file type. Please upload Excel (.xlsx, .xls) or CSV files.'}), 400

# Get and validate model parameter
model_id = request.form.get('model')
if not model_id:
# Use default model if not provided
config = _load_ai_models()
model_id = config.get('default_model', 'gpt4omni')

# Validate model
model_validation = _validate_model(model_id)
if not model_validation['valid']:
return jsonify({'error': model_validation['error']}), 400

model_config = model_validation['model_config']

filename = secure_filename(file.filename)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
name, ext = os.path.splitext(filename)
Expand Down Expand Up @@ -1289,15 +1399,16 @@ def upload_compare():
]
}

thread = threading.Thread(target=process_compare_background, args=(processing_id, unique_filename))
thread = threading.Thread(target=process_compare_background, args=(processing_id, unique_filename, model_id))
thread.daemon = True
thread.start()

return jsonify({
'message': 'Comparison started',
'processing_id': processing_id,
'filename': unique_filename,
'file_info': file_info
'file_info': file_info,
'model': model_config.get('displayName')
}), 200
except Exception as e:
return jsonify({'error': f'Upload compare failed: {str(e)}'}), 500
Expand All @@ -1312,13 +1423,25 @@ def process_address():
if not address:
return jsonify({'error': 'Address is required'}), 400

# Get and validate model parameter
model_id = data.get('model')
if not model_id:
# Use default model if not provided
config = _load_ai_models()
model_id = config.get('default_model', 'gpt4omni')

# Validate model
model_validation = _validate_model(model_id)
if not model_validation['valid']:
return jsonify({'error': model_validation['error']}), 400

# Import the CSV processor to use its address standardization
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from csv_address_processor import CSVAddressProcessor

processor = CSVAddressProcessor()
processor = CSVAddressProcessor(model=model_id)
result = processor.standardize_single_address(address, 0) # row_index 0 for single address

# Check if Azure OpenAI processing was successful
Expand Down Expand Up @@ -1393,10 +1516,22 @@ def process_addresses():
if not addresses or not isinstance(addresses, list):
return jsonify({'error': 'addresses (list) is required'}), 400

# Get and validate model parameter
model_id = data.get('model')
if not model_id:
# Use default model if not provided
config = _load_ai_models()
model_id = config.get('default_model', 'gpt4omni')

# Validate model
model_validation = _validate_model(model_id)
if not model_validation['valid']:
return jsonify({'error': model_validation['error']}), 400

import sys, os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from csv_address_processor import CSVAddressProcessor
processor = CSVAddressProcessor()
processor = CSVAddressProcessor(model=model_id)

results = []
for idx, raw in enumerate(addresses):
Expand Down
25 changes: 15 additions & 10 deletions chatbot-app/backend/app/services/azure_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ def get_access_token():
else:
raise Exception(f'Failed to obtain access token: {response.text}')

def connect_wso2(access_token, user_content: str, system_prompt: str = None, prompt_type: str = "general", max_tokens: int = None):
deployment_id = os.getenv("AZURE_OPENAI_DEPLOYMENT_ID", "AOAIsharednonprodgpt35turbo16k")
def connect_wso2(access_token, user_content: str, system_prompt: str = None, prompt_type: str = "general", max_tokens: int = None, model: str = None):
# Use provided model or fallback to environment variable
deployment_id = model if model else os.getenv("AZURE_OPENAI_DEPLOYMENT_ID", "gpt4omni")
api_version = '2024-02-15-preview'
proxy_url = 'https://api-test.cbre.com:443/t/digitaltech_us_edp/cbreopenaiendpoint/1/openai/deployments/{deployment_id}/chat/completions'
url_variable = proxy_url.format(deployment_id=deployment_id)
Expand Down Expand Up @@ -329,7 +330,7 @@ def ensure_unicode_safe_content(content: str) -> str:
# Replace problematic characters with safe alternatives
return content.encode('utf-8', errors='replace').decode('utf-8')

def standardize_address(raw_address: str, target_country: str = None):
def standardize_address(raw_address: str, target_country: str = None, model: str = None):
"""
Convenience function specifically for address standardization with optional country-specific formatting

Expand Down Expand Up @@ -358,7 +359,8 @@ def standardize_address(raw_address: str, target_country: str = None):
response = connect_wso2(
access_token=access_token,
user_content=enhanced_content,
prompt_type="address_standardization"
prompt_type="address_standardization",
model=model
)

# Extract the content from OpenAI response
Expand Down Expand Up @@ -415,14 +417,15 @@ def standardize_address(raw_address: str, target_country: str = None):
except Exception as e:
return {"error": str(e)}

def standardize_multiple_addresses(address_list: list, target_country: str = None, use_batch: bool = True):
def standardize_multiple_addresses(address_list: list, target_country: str = None, use_batch: bool = True, model: str = None):
"""
Standardize multiple addresses efficiently using batch processing

Args:
address_list (list): List of raw address strings
target_country (str, optional): Target country for country-specific formatting
use_batch (bool): Whether to use batch processing (True) or individual calls (False)
model (str, optional): AI model to use for standardization

Returns:
list: List of standardized addresses with input_index for matching
Expand All @@ -439,7 +442,7 @@ def standardize_multiple_addresses(address_list: list, target_country: str = Non
print(f"🔄 Processing {len(address_list)} addresses individually...")
standardized_addresses = []
for i, address in enumerate(address_list):
result = standardize_address(address, target_country)
result = standardize_address(address, target_country, model)
result['input_index'] = i # Add index for matching
standardized_addresses.append(result)
return standardized_addresses
Expand All @@ -456,15 +459,15 @@ def standardize_multiple_addresses(address_list: list, target_country: str = Non

try:
# Process this batch
batch_results = _process_address_batch(batch_addresses, target_country, batch_start)
batch_results = _process_address_batch(batch_addresses, target_country, batch_start, model)
all_results.extend(batch_results)

except Exception as e:
print(f" ❌ Batch failed, falling back to individual processing: {str(e)}")
# Fallback to individual processing for this batch
for i, address in enumerate(batch_addresses):
try:
result = standardize_address(address, target_country)
result = standardize_address(address, target_country, model)
result['input_index'] = batch_start + i
all_results.append(result)
except Exception as individual_error:
Expand All @@ -482,14 +485,15 @@ def standardize_multiple_addresses(address_list: list, target_country: str = Non
print(f"✅ Batch processing completed: {len(all_results)} addresses processed")
return all_results

def _process_address_batch(address_list: list, target_country: str = None, batch_offset: int = 0):
def _process_address_batch(address_list: list, target_country: str = None, batch_offset: int = 0, model: str = None):
"""
Process a batch of addresses in a single API call

Args:
address_list (list): List of addresses in this batch
target_country (str, optional): Target country for formatting
batch_offset (int): Offset for input_index calculation
model (str, optional): AI model to use

Returns:
list: List of standardized addresses
Expand Down Expand Up @@ -525,7 +529,8 @@ def _process_address_batch(address_list: list, target_country: str = None, batch
access_token=access_token,
user_content=enhanced_content,
system_prompt=system_prompt,
max_tokens=3000 # Higher token limit for batch processing to handle 5 addresses
max_tokens=3000, # Higher token limit for batch processing to handle 5 addresses
model=model
)

# Extract and parse the batch response
Expand Down
Loading