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
16 changes: 15 additions & 1 deletion application/single_app/functions_agent_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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))
36 changes: 23 additions & 13 deletions application/single_app/semantic_kernel_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
)
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
Expand Down Expand Up @@ -2097,24 +2097,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

Expand Down
57 changes: 57 additions & 0 deletions docs/explanation/fixes/v0.241.007/GLOBAL_AGENT_SCOPE_GATE_FIX.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 6 additions & 12 deletions docs/explanation/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,13 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Ver

### **(v0.241.007)**

#### New Features
#### Bug Fixes

* **Collaborative Conversations Foundation**
* Added the first backend foundation for multi-user collaborative conversations with dedicated conversation, message, and per-user membership state storage in Cosmos DB.
* Added protected APIs for creating personal or group collaborative conversations, accepting invites, inviting or removing members in personal conversations, posting human messages, publishing typing events, and subscribing to a conversation-wide SSE event stream.
* This initial slice keeps the existing single-user chat experience intact while establishing the persistence and eventing layer needed for shared conversation UI, explicit AI invocation, and future read-state improvements.
* (Ref: `collaboration_models.py`, `functions_collaboration.py`, `route_backend_collaboration.py`, `config.py`, `test_collaborative_conversation_foundation.py`)

* **Core Document Search And Summarization**
* Added a shared backend document search service with a dedicated authenticated API for hybrid search, ordered document-chunk retrieval, and on-demand document summarization.
* Added an always-loaded Semantic Kernel core plugin so every agent and model-only kernel session can search accessible workspace documents, pull full ordered chunk windows for a document, and run hierarchical summarization with optional focus and target-length guidance.
* The new summarization flow can now work across the whole document instead of relying only on distilled top search hits, which improves long-document summarization and creates a reusable foundation for future document comparison workflows.
* (Ref: `functions_search.py`, `functions_search_service.py`, `functions_documents.py`, `route_backend_search.py`, `document_search_plugin.py`, `semantic_kernel_loader.py`)
* **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)**

Expand Down
118 changes: 118 additions & 0 deletions functional_tests/test_global_agent_scope_gate.py
Original file line number Diff line number Diff line change
@@ -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)
Loading