From e0a8adfe7ad5727385050ae2e10f78a9da78ed9e Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Fri, 1 May 2026 07:58:32 -0400 Subject: [PATCH] fix global agent bug global agent was being classified as personal which meant that the user agent needed to be enabled for global agents to work too. --- application/single_app/config.py | 2 +- .../single_app/functions_agent_scope.py | 16 ++- .../single_app/semantic_kernel_loader.py | 36 ++++-- .../v0.241.007/GLOBAL_AGENT_SCOPE_GATE_FIX.md | 57 +++++++++ docs/explanation/release_notes.md | 10 ++ .../test_global_agent_scope_gate.py | 118 ++++++++++++++++++ 6 files changed, 224 insertions(+), 15 deletions(-) create mode 100644 docs/explanation/fixes/v0.241.007/GLOBAL_AGENT_SCOPE_GATE_FIX.md create mode 100644 functional_tests/test_global_agent_scope_gate.py diff --git a/application/single_app/config.py b/application/single_app/config.py index 7196cfe8..3ccb6ca9 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.241.006" +VERSION = "0.241.007" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/functions_agent_scope.py b/application/single_app/functions_agent_scope.py index 660647b9..526c7d58 100644 --- a/application/single_app/functions_agent_scope.py +++ b/application/single_app/functions_agent_scope.py @@ -30,4 +30,18 @@ def scope_matches(candidate): if selected_agent_name: return next((agent for agent in agents_cfg if agent.get("name") == selected_agent_name and scope_matches(agent)), None) - return None \ No newline at end of file + return None + + +def is_selected_agent_scope_enabled(settings, selected_agent_data): + """Return whether app settings allow the selected agent's scope.""" + if not isinstance(selected_agent_data, dict): + return True + + if selected_agent_data.get("is_group", False): + return bool((settings or {}).get("allow_group_agents", False)) + + if selected_agent_data.get("is_global", False): + return True + + return bool((settings or {}).get("allow_user_agents", False)) \ No newline at end of file diff --git a/application/single_app/semantic_kernel_loader.py b/application/single_app/semantic_kernel_loader.py index 3a2ca4b5..f66cfca7 100644 --- a/application/single_app/semantic_kernel_loader.py +++ b/application/single_app/semantic_kernel_loader.py @@ -47,7 +47,7 @@ from functions_agent_payload import can_agent_use_default_multi_endpoint_model from semantic_kernel_plugins.plugin_loader import discover_plugins from semantic_kernel_plugins.openapi_plugin_factory import OpenApiPluginFactory -from functions_agent_scope import find_agent_by_scope +from functions_agent_scope import find_agent_by_scope, is_selected_agent_scope_enabled import app_settings_cache # Agent and Azure OpenAI chat service imports @@ -1897,24 +1897,34 @@ def load_user_semantic_kernel(kernel: Kernel, settings, user_id: str, redis_clie # Append selected group agent (if any) to the candidate list so downstream selection logic can resolve it selected_agent_data = selected_agent if isinstance(selected_agent, dict) else {} + selected_agent_is_global = selected_agent_data.get('is_global', False) selected_agent_is_group = selected_agent_data.get('is_group', False) selected_agent_group_id = selected_agent_data.get('group_id') conversation_group_id = getattr(g, "conversation_group_id", None) allow_user_agents = settings.get('allow_user_agents', False) allow_group_agents = settings.get('allow_group_agents', False) - if selected_agent_is_group and not allow_group_agents: - log_event( - "[SK Loader] Group agents are disabled; skipping group agent load.", - level=logging.WARNING - ) - load_core_plugins_only(kernel, settings) - return kernel, None - if not selected_agent_is_group and not allow_user_agents: - log_event( - "[SK Loader] User agents are disabled; skipping personal agent load.", - level=logging.WARNING - ) + if not is_selected_agent_scope_enabled(settings, selected_agent_data): + if selected_agent_is_group: + log_event( + "[SK Loader] Group agents are disabled; skipping group agent load.", + level=logging.WARNING, + extra={ + 'agent_name': selected_agent_data.get('name'), + 'allow_group_agents': allow_group_agents, + 'is_global': selected_agent_is_global, + } + ) + else: + log_event( + "[SK Loader] User agents are disabled; skipping personal agent load.", + level=logging.WARNING, + extra={ + 'agent_name': selected_agent_data.get('name'), + 'allow_user_agents': allow_user_agents, + 'is_global': selected_agent_is_global, + } + ) load_core_plugins_only(kernel, settings) return kernel, None diff --git a/docs/explanation/fixes/v0.241.007/GLOBAL_AGENT_SCOPE_GATE_FIX.md b/docs/explanation/fixes/v0.241.007/GLOBAL_AGENT_SCOPE_GATE_FIX.md new file mode 100644 index 00000000..6bb9cfc4 --- /dev/null +++ b/docs/explanation/fixes/v0.241.007/GLOBAL_AGENT_SCOPE_GATE_FIX.md @@ -0,0 +1,57 @@ +# GLOBAL_AGENT_SCOPE_GATE_FIX.md + +## Global Agent Scope Gate Fix (v0.241.007) + +Fixed/Implemented in version: **0.241.007** + +### Issue Description + +Per-user Semantic Kernel chats could silently fall back to the standard GPT model +when a user selected a global agent from the chat UI. The frontend showed no +error because the selection API accepted the agent and the streaming request +included that `agent_info`, but the backend still dropped into model-only mode. + +### Root Cause Analysis + +The per-user loader treated every non-group agent as a personal agent during the +scope gate check. When `allow_user_agents` was disabled and +`merge_global_semantic_kernel_with_workspace` was enabled, selected global agents +were blocked before the loader reached the global-agent merge and selection path. + +### Technical Details + +Files modified: +- `application/single_app/functions_agent_scope.py` +- `application/single_app/semantic_kernel_loader.py` +- `application/single_app/config.py` +- `functional_tests/test_global_agent_scope_gate.py` + +Code changes summary: +- Added `is_selected_agent_scope_enabled()` to centralize scope gating for + personal, global, and group agent selections. +- Updated `load_user_semantic_kernel()` so global agents bypass the + `allow_user_agents` toggle while personal and group agent rules remain intact. +- Added regression coverage for the global-agent bypass, group-agent enforcement, + and loader wiring. + +Testing approach: +- Added `functional_tests/test_global_agent_scope_gate.py` to validate the scope + helper behavior and confirm the per-user loader uses it. + +Impact analysis: +- Global agents selected in per-user chat mode now remain on the agent invocation + path instead of silently reverting to model-only GPT routing. +- Personal and group scope restrictions continue to behave as configured. + +### Validation + +Before: +- The backend logged `Using agent from request` and then immediately logged + `User agents are disabled; skipping personal agent load.` for global agents. +- Requests fell back to `Loading core plugins only for model-only mode...`. + +After: +- Global agent selections are no longer blocked by the personal-agent gate. +- Group selections still require `allow_group_agents`, and personal selections + still require `allow_user_agents`. +- The regression test protects the shared scope gate and its loader integration. \ No newline at end of file diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index da34cbad..bdc10fe4 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -4,6 +4,16 @@ This page tracks notable Simple Chat releases and organizes the detailed change For feature-focused and fix-focused drill-downs by version, see [Features by Version](/explanation/features/) and [Fixes by Version](/explanation/fixes/). +### **(v0.241.007)** + +#### Bug Fixes + +* **Global Agent Scope Gate Fallback** + * Fixed per-user Semantic Kernel chats so selecting a global agent no longer silently falls back to the standard GPT model when personal agents are disabled for the tenant. + * The per-user loader now treats global, personal, and group agent scopes separately, allowing valid global-agent selections to continue through agent invocation while keeping personal and group scope toggles enforced as configured. + * Added regression coverage for the shared scope gate used by the per-user loader. + * (Ref: `semantic_kernel_loader.py`, `functions_agent_scope.py`, `test_global_agent_scope_gate.py`, global agent request routing) + ### **(v0.241.006)** #### Bug Fixes diff --git a/functional_tests/test_global_agent_scope_gate.py b/functional_tests/test_global_agent_scope_gate.py new file mode 100644 index 00000000..0f7d21b9 --- /dev/null +++ b/functional_tests/test_global_agent_scope_gate.py @@ -0,0 +1,118 @@ +# test_global_agent_scope_gate.py +""" +Functional test for global agent scope gating in per-user Semantic Kernel mode. +Version: 0.241.007 +Implemented in: 0.241.007 + +This test ensures global agents remain eligible for loading even when personal +agent access is disabled, while personal and group scopes still respect their +own admin toggles. +""" + +import os +import sys + + +repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(repo_root) + +from application.single_app.functions_agent_scope import is_selected_agent_scope_enabled + + +def read_file_text(file_path): + with open(file_path, "r", encoding="utf-8") as file: + return file.read() + + +def test_global_agents_bypass_personal_toggle(): + """Ensure global agents are not blocked by the personal-agent toggle.""" + print("๐Ÿ” Validating global agent scope bypass...") + + settings = { + "allow_user_agents": False, + "allow_group_agents": False, + } + global_agent = { + "name": "beta_occ_document_summarization_agent", + "is_global": True, + "is_group": False, + } + personal_agent = { + "name": "personal-agent", + "is_global": False, + "is_group": False, + } + + assert is_selected_agent_scope_enabled(settings, global_agent) is True + assert is_selected_agent_scope_enabled(settings, personal_agent) is False + + print("โœ… Global agent scope bypass passed.") + + +def test_group_agents_still_require_group_toggle(): + """Ensure group agents still honor the group-agent toggle.""" + print("๐Ÿ” Validating group agent scope enforcement...") + + settings = { + "allow_user_agents": True, + "allow_group_agents": False, + } + group_agent = { + "name": "group-agent", + "is_global": False, + "is_group": True, + "group_id": "group-a", + } + + assert is_selected_agent_scope_enabled(settings, group_agent) is False + + settings["allow_group_agents"] = True + assert is_selected_agent_scope_enabled(settings, group_agent) is True + + print("โœ… Group agent scope enforcement passed.") + + +def test_loader_uses_scope_gate_helper(): + """Ensure the per-user loader uses the shared scope gate helper.""" + print("๐Ÿ” Validating loader wiring for shared scope gate helper...") + + loader_path = os.path.join( + repo_root, "application", "single_app", "semantic_kernel_loader.py" + ) + loader_text = read_file_text(loader_path) + + assert "is_selected_agent_scope_enabled(settings, selected_agent_data)" in loader_text, ( + "Expected semantic kernel loader to use the shared selected-agent scope helper." + ) + + print("โœ… Loader wiring for scope gate helper passed.") + + +def run_tests(): + tests = [ + test_global_agents_bypass_personal_toggle, + test_group_agents_still_require_group_toggle, + test_loader_uses_scope_gate_helper, + ] + results = [] + + for test in tests: + print(f"\n๐Ÿงช Running {test.__name__}...") + try: + test() + print("โœ… Test passed") + results.append(True) + except Exception as exc: + print(f"โŒ Test failed: {exc}") + import traceback + + traceback.print_exc() + results.append(False) + + success = all(results) + print(f"\n๐Ÿ“Š Results: {sum(results)}/{len(results)} tests passed") + return success + + +if __name__ == "__main__": + raise SystemExit(0 if run_tests() else 1) \ No newline at end of file