Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ The application utilizes **Azure Cosmos DB** for storing conversations, metadata

- Text: `txt`, `md`, `html`, `json`

* Documents: `pdf`, `docx`, `pptx`, `xlsx`, `xls`, `csv`
* Documents: `pdf`, `docx`, `pptx`, `xlsx`, `xlsm`, `xls`, `csv`
* Images: `jpg`, `jpeg`, `png`, `bmp`, `tiff`, `tif`, `heif` (processed via Document Intelligence OCR)
* Video: `mp4`, `mov`, `avi`, `wmv`, `mkv`, `webm` (requires Video Indexer)
* Audio: `mp3`, `wav`, `ogg`, `aac`, `flac`, `m4a` (requires Speech Service)
Expand Down
15 changes: 14 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
<!-- BEGIN RELEASE_NOTES.MD BLOCK -->
# Feature Release

### **(v0.229.060)**
### **(v0.229.062)**

#### Bug Fixes

* **Enhanced Citations CSP Fix**
* Fixed Content Security Policy (CSP) violation that prevented enhanced citations PDF documents from being displayed in iframe modals.
* **Issue**: CSP directive `frame-ancestors 'none'` blocked PDF endpoints from being embedded in iframes, causing console errors: "Refused to frame '...' because an ancestor violates the following Content Security Policy directive: 'frame-ancestors 'none''".
* **Root Cause**: Enhanced citations use iframes to display PDF documents via `/api/enhanced_citations/pdf` endpoint, but the restrictive CSP policy prevented same-origin iframe embedding.
* **Solution**: Changed CSP configuration from `frame-ancestors 'none'` to `frame-ancestors 'self'`, allowing same-origin framing while maintaining security against external clickjacking attacks.
* **Security Impact**: No reduction in security posture - external websites still cannot embed application content, only same-origin framing is now allowed.
* **Benefits**: Enhanced citations PDF modals now display correctly without CSP violations, improved user experience for document viewing.
* (Ref: `config.py` SECURITY_HEADERS, `test_enhanced_citations_csp_fix.py`, CSP policy update)

### **(v0.229.061)**

#### Bug Fixes

Expand Down
4 changes: 4 additions & 0 deletions application/single_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
# Register Enhanced Citations routes
register_enhanced_citations_routes(app)

# Register Swagger documentation routes
from swagger_wrapper import register_swagger_routes
register_swagger_routes(app)

from flask import g
from flask_session import Session
from redis import Redis
Expand Down
4 changes: 2 additions & 2 deletions application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.229.104"
VERSION = "0.230.001"

SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')

Expand Down Expand Up @@ -120,7 +120,7 @@
CLIENTS_LOCK = threading.Lock()

ALLOWED_EXTENSIONS = {
'txt', 'pdf', 'docx', 'xlsx', 'xls', 'csv', 'pptx', 'html', 'jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif', 'heif', 'md', 'json',
'txt', 'pdf', 'docx', 'xlsx', 'xlsm', 'xls', 'csv', 'pptx', 'html', 'jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif', 'heif', 'md', 'json',
'mp4', 'mov', 'avi', 'mkv', 'flv', 'mxf', 'gxf', 'ts', 'ps', '3gp', '3gpp', 'mpg', 'wmv', 'asf', 'm4a', 'm4v', 'isma', 'ismv',
'dvr-ms', 'wav'
}
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/functions_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def extract_table_file(file_path, file_ext):
try:
if file_ext == '.csv':
df = pandas.read_csv(file_path)
elif file_ext in ['.xls', '.xlsx']:
elif file_ext in ['.xls', '.xlsx', '.xlsm']:
df = pandas.read_excel(file_path)
else:
raise ValueError("Unsupported file extension for table extraction.")
Expand Down
8 changes: 4 additions & 4 deletions application/single_app/functions_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -3045,7 +3045,7 @@ def process_single_tabular_sheet(df, document_id, user_id, file_name, update_cal
return total_chunks_saved

def process_tabular(document_id, user_id, temp_file_path, original_filename, file_ext, enable_enhanced_citations, update_callback, group_id=None, public_workspace_id=None):
"""Processes CSV, XLSX, or XLS files using pandas."""
"""Processes CSV, XLSX, XLSM, or XLS files using pandas."""
is_group = group_id is not None
is_public_workspace = public_workspace_id is not None

Expand Down Expand Up @@ -3093,11 +3093,11 @@ def process_tabular(document_id, user_id, temp_file_path, original_filename, fil

total_chunks_saved = process_single_tabular_sheet(**args)

elif file_ext in ('.xlsx', '.xls'):
elif file_ext in ('.xlsx', '.xls', '.xlsm'):
# Process Excel (potentially multiple sheets)
excel_file = pandas.ExcelFile(
temp_file_path,
engine='openpyxl' if file_ext == '.xlsx' else 'xlrd'
engine='openpyxl' if file_ext == '.xlsx' or file_ext == '.xlsm' else 'xlrd'
)
sheet_names = excel_file.sheet_names
base_name, ext = os.path.splitext(original_filename)
Expand Down Expand Up @@ -3632,7 +3632,7 @@ def update_doc_callback(**kwargs):

# --- 1. Dispatch to appropriate handler based on file type ---
di_supported_extensions = ('.pdf', '.docx', '.doc', '.pptx', '.ppt', '.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.heif')
tabular_extensions = ('.csv', '.xlsx', '.xls')
tabular_extensions = ('.csv', '.xlsx', '.xlsm', '.xls')

is_group = group_id is not None

Expand Down
13 changes: 13 additions & 0 deletions application/single_app/plugin_validation_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
from semantic_kernel_plugins.plugin_loader import discover_plugins
from functions_appinsights import log_event
from functions_authentication import login_required, admin_required
from swagger_wrapper import swagger_route, get_auth_security
import logging


plugin_validation_bp = Blueprint('plugin_validation', __name__)


@plugin_validation_bp.route('/api/admin/plugins/validate', methods=['POST'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def validate_plugin_manifest():
Expand Down Expand Up @@ -60,6 +64,9 @@ def validate_plugin_manifest():


@plugin_validation_bp.route('/api/admin/plugins/test-instantiation', methods=['POST'])
@swagger_route(
security=get_auth_security()
)
def test_plugin_instantiation():
"""
Test if a plugin can be instantiated successfully.
Expand Down Expand Up @@ -128,6 +135,9 @@ def normalize(s):


@plugin_validation_bp.route('/api/admin/plugins/health-check/<plugin_name>', methods=['GET'])
@swagger_route(
security=get_auth_security()
)
def check_plugin_health(plugin_name):
"""
Perform a health check on an existing plugin.
Expand Down Expand Up @@ -201,6 +211,9 @@ def normalize(s):


@plugin_validation_bp.route('/api/admin/plugins/repair/<plugin_name>', methods=['POST'])
@swagger_route(
security=get_auth_security()
)
def repair_plugin(plugin_name):
"""
Attempt to repair a plugin that has issues.
Expand Down
49 changes: 49 additions & 0 deletions application/single_app/route_backend_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,25 @@
from functions_authentication import *
from functions_appinsights import log_event
from json_schema_validation import validate_agent
from swagger_wrapper import swagger_route, get_auth_security

bpa = Blueprint('admin_agents', __name__)

# === AGENT GUID GENERATION ENDPOINT ===
@bpa.route('/api/agents/generate_id', methods=['GET'])
@swagger_route(
security=get_auth_security()
)
@login_required
def generate_agent_id():
"""Generate a new GUID for agent creation (user or admin)."""
return jsonify({'id': str(uuid.uuid4())})

# === USER AGENTS ENDPOINTS ===
@bpa.route('/api/user/agents', methods=['GET'])
@swagger_route(
security=get_auth_security()
)
@login_required
def get_user_agents():
user_id = get_current_user_id()
Expand Down Expand Up @@ -71,6 +78,9 @@ def get_user_agents():
return jsonify(agents)

@bpa.route('/api/user/agents', methods=['POST'])
@swagger_route(
security=get_auth_security()
)
@login_required
@enabled_required("allow_user_agents")
def set_user_agents():
Expand Down Expand Up @@ -134,6 +144,9 @@ def set_user_agents():

# Add a DELETE endpoint for user agents (if not present)
@bpa.route('/api/user/agents/<agent_name>', methods=['DELETE'])
@swagger_route(
security=get_auth_security()
)
@enabled_required("allow_user_agents")
@login_required
def delete_user_agent(agent_name):
Expand Down Expand Up @@ -170,6 +183,9 @@ def delete_user_agent(agent_name):

# User endpoint to set selected agent (new model, not legacy default_agent)
@bpa.route('/api/user/settings/selected_agent', methods=['POST'])
@swagger_route(
security=get_auth_security()
)
@login_required
def set_user_selected_agent():
user_id = get_current_user_id()
Expand All @@ -189,18 +205,27 @@ def set_user_selected_agent():
return jsonify({'success': True})

@bpa.route('/api/user/agent/settings', methods=['GET'])
@swagger_route(
security=get_auth_security()
)
@login_required
def get_global_agent_settings_for_users():
return get_global_agent_settings(include_admin_extras=False)

# === ADMIN AGENTS ENDPOINTS ===
@bpa.route('/api/admin/agent/settings', methods=['GET'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def get_all_admin_settings():
return get_global_agent_settings(include_admin_extras=True)

@bpa.route('/api/admin/agents/selected_agent', methods=['POST'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def set_selected_agent():
Expand Down Expand Up @@ -233,6 +258,9 @@ def set_selected_agent():


@bpa.route('/api/admin/agents', methods=['GET'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def list_agents():
Expand All @@ -256,6 +284,9 @@ def list_agents():
return jsonify({'error': 'Failed to list agents.'}), 500

@bpa.route('/api/admin/agents', methods=['POST'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def add_agent():
Expand Down Expand Up @@ -303,6 +334,9 @@ def add_agent():
return jsonify({'error': 'Failed to add agent.'}), 500

@bpa.route('/api/admin/agents/settings/<setting_name>', methods=['GET'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def get_admin_agent_settings(setting_name):
Expand All @@ -312,6 +346,9 @@ def get_admin_agent_settings(setting_name):

# Add a generic agent settings update route for simple values
@bpa.route('/api/admin/agents/settings/<setting_name>', methods=['POST'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def update_agent_setting(setting_name):
Expand Down Expand Up @@ -356,6 +393,9 @@ def update_agent_setting(setting_name):
return jsonify({'error': 'Failed to update agent setting.'}), 500

@bpa.route('/api/admin/agents/<agent_name>', methods=['PUT'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def edit_agent(agent_name):
Expand Down Expand Up @@ -418,6 +458,9 @@ def edit_agent(agent_name):
return jsonify({'error': 'Failed to edit agent.'}), 500

@bpa.route('/api/admin/agents/<agent_name>', methods=['DELETE'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def delete_agent(agent_name):
Expand Down Expand Up @@ -451,13 +494,19 @@ def delete_agent(agent_name):
return jsonify({'error': 'Failed to delete agent.'}), 500

@bpa.route('/api/orchestration_types', methods=['GET'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def orchestration_types():
"""Return the available orchestration types (full metadata)."""
return jsonify(get_agent_orchestration_types())

@bpa.route('/api/orchestration_settings', methods=['GET', 'POST'])
@swagger_route(
security=get_auth_security()
)
@login_required
@admin_required
def orchestration_settings():
Expand Down
4 changes: 4 additions & 0 deletions application/single_app/route_backend_chats.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from functions_conversation_metadata import collect_conversation_metadata, update_conversation_with_metadata
from functions_debug import debug_print
from flask import current_app
from swagger_wrapper import swagger_route, get_auth_security


def get_kernel():
Expand All @@ -35,6 +36,9 @@ def get_kernel_agents():

def register_route_backend_chats(app):
@app.route('/api/chat', methods=['POST'])
@swagger_route(
security=get_auth_security()
)
@login_required
@user_required
def chat_api():
Expand Down
9 changes: 9 additions & 0 deletions application/single_app/route_backend_conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
from functions_conversation_metadata import get_conversation_metadata
from flask import Response, request
from functions_debug import debug_print
from swagger_wrapper import swagger_route, get_auth_security

def register_route_backend_conversations(app):

@app.route('/api/get_messages', methods=['GET'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def api_get_messages():
Expand Down Expand Up @@ -112,6 +114,7 @@ def api_get_messages():
return jsonify({'error': 'Conversation not found'}), 404

@app.route('/api/image/<image_id>', methods=['GET'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def api_get_image(image_id):
Expand Down Expand Up @@ -237,6 +240,7 @@ def api_get_image(image_id):
return jsonify({'error': 'Failed to retrieve image'}), 500

@app.route('/api/get_conversations', methods=['GET'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def get_conversations():
Expand All @@ -251,6 +255,7 @@ def get_conversations():


@app.route('/api/create_conversation', methods=['POST'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def create_conversation():
Expand All @@ -276,6 +281,7 @@ def create_conversation():
}), 200

@app.route('/api/conversations/<conversation_id>', methods=['PUT'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def update_conversation_title(conversation_id):
Expand Down Expand Up @@ -320,6 +326,7 @@ def update_conversation_title(conversation_id):
return jsonify({'error': 'Failed to update conversation'}), 500

@app.route('/api/conversations/<conversation_id>', methods=['DELETE'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def delete_conversation(conversation_id):
Expand Down Expand Up @@ -378,6 +385,7 @@ def delete_conversation(conversation_id):
}), 200

@app.route('/api/delete_multiple_conversations', methods=['POST'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def delete_multiple_conversations():
Expand Down Expand Up @@ -458,6 +466,7 @@ def delete_multiple_conversations():
}), 200

@app.route('/api/conversations/<conversation_id>/metadata', methods=['GET'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def get_conversation_metadata_api(conversation_id):
Expand Down
Loading