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
33 changes: 26 additions & 7 deletions application/single_app/agent_logging_chat_completion.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

# agent_logging_chat_completion.py
import json
import logging
from pydantic import Field
Expand All @@ -15,6 +15,7 @@ class LoggingChatCompletionAgent(ChatCompletionAgent):
deployment_name: str | None = Field(default=None)
azure_endpoint: str | None = Field(default=None)
api_version: str | None = Field(default=None)
orchestration_minimize_telemetry: bool = Field(default=False, exclude=True)

def __init__(self, *args, display_name=None, default_agent=False, deployment_name=None, azure_endpoint=None, api_version=None, **kwargs):
# Remove these from kwargs so the base class doesn't see them
Expand All @@ -33,10 +34,11 @@ def __init__(self, *args, display_name=None, default_agent=False, deployment_nam

def log_tool_execution(self, tool_name, arguments=None, result=None):
"""Manual method to log tool executions. Can be called by plugins."""
minimize_telemetry = self.orchestration_minimize_telemetry
tool_citation = {
"tool_name": tool_name,
"function_arguments": str(arguments) if arguments else "",
"function_result": str(result)[:500] if result else "",
"function_arguments": "" if minimize_telemetry else str(arguments) if arguments else "",
"function_result": "" if minimize_telemetry else str(result)[:500] if result else "",
"timestamp": datetime.datetime.utcnow().isoformat()
}
self.tool_invocations.append(tool_citation)
Expand Down Expand Up @@ -136,11 +138,15 @@ async def invoke(self, *args, **kwargs):
self.tool_invocations = []

# Log the prompt/messages before sending to LLM
minimize_telemetry = self.orchestration_minimize_telemetry
prompt_messages = args[0] if args and isinstance(args[0], (list, tuple)) else []
log_event(
"[Logging Agent Request] Agent LLM prompt",
extra={
"agent": self.name,
"prompt": [m.content[:30] for m in args[0]] if args else None
"prompt": None if minimize_telemetry else [m.content[:30] for m in prompt_messages],
"prompt_message_count": len(prompt_messages),
"telemetry_minimized": minimize_telemetry,
}
)

Expand Down Expand Up @@ -180,15 +186,22 @@ async def invoke(self, *args, **kwargs):
extra={
"agent": self.name,
"response_type": type(response).__name__,
"response_preview": str(response)[:100] if response else None
"response_preview": (
None
if minimize_telemetry
else str(response)[:100] if response else None
),
"response_length": len(str(response)) if response else 0,
"telemetry_minimized": minimize_telemetry,
},
level=logging.DEBUG
)

# Store the response for analysis
self._last_response = response
# Simplified citation capture - primary citations come from plugin invocation logger
self._capture_tool_invocations_simplified(args, response)
if not minimize_telemetry:
self._capture_tool_invocations_simplified(args, response)

return response
finally:
Expand All @@ -197,7 +210,13 @@ async def invoke(self, *args, **kwargs):
"[Logging Agent Response][Usage] Agent LLM response",
extra={
"agent": self.name,
"response": str(response)[:100] if response else None,
"response": (
None
if minimize_telemetry
else str(response)[:100] if response else None
),
"response_length": len(str(response)) if response else 0,
"telemetry_minimized": minimize_telemetry,
"prompt_tokens": getattr(usage, "prompt_tokens", None),
"completion_tokens": getattr(usage, "completion_tokens", None),
"total_tokens": getattr(usage, "total_tokens", None),
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.250.066"
VERSION = "0.250.067"

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
SESSION_COOKIE_HTTPONLY = os.getenv('SESSION_COOKIE_HTTPONLY', 'true').lower() != 'false'
Expand Down
161 changes: 159 additions & 2 deletions application/single_app/functions_agent_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@
from functions_assigned_knowledge import get_agent_assigned_knowledge
from functions_global_actions import get_global_actions
from functions_global_agents import get_global_agents
from functions_group import get_group_model_endpoints, get_user_groups
from functions_group import (
check_group_status_allows_operation,
get_group_model_endpoints,
get_user_groups,
)
from functions_group_actions import get_group_actions
from functions_group_agents import get_group_agents
from functions_governance import filter_actions_by_action_type_access, filter_governed_global_actions_for_user
from functions_governance import (
ensure_governance_access,
filter_actions_by_action_type_access,
filter_governed_global_actions_for_user,
)
from functions_keyvault import SecretReturnType
from functions_personal_actions import get_governed_personal_actions
from functions_personal_agents import ensure_migration_complete, get_personal_agents
Expand Down Expand Up @@ -272,6 +280,155 @@ def _should_include_global_agents(settings: Dict[str, Any]) -> bool:
return bool(settings.get("enable_semantic_kernel", False))


def _should_include_global_agents_for_discovery(settings: Dict[str, Any]) -> bool:
if not _should_include_global_agents(settings):
return False
return bool(
not settings.get("per_user_semantic_kernel", False)
or settings.get("merge_global_semantic_kernel_with_workspace", False)
)


def _is_agent_currently_discoverable(agent: Dict[str, Any]) -> bool:
if not isinstance(agent, dict):
return False
if str(agent.get("agent_type") or "local").strip().lower() != "local":
return False
if not str(agent.get("created_at") or "").strip():
return False
if any(
str(action_id or "").strip()
for action_id in (agent.get("actions_to_load") or [])
):
return False
if agent.get("is_enabled") is False:
return False
if agent.get("hidden") is True or agent.get("is_hidden") is True:
return False
if str(agent.get("visibility") or "").strip().lower() == "hidden":
return False
if str(agent.get("status") or "").strip().lower() in {"disabled", "inactive", "hidden"}:
return False
discoverable = agent.get("discoverable_by_orchestrator", False)
if isinstance(discoverable, str):
discoverable = discoverable.strip().lower() == "true"
return bool(discoverable and isinstance(agent.get("orchestrator_descriptor"), dict))


def _is_agent_allowed_by_governance(user_id: str, agent: Dict[str, Any], scope_type: str) -> bool:
try:
if scope_type == "global":
ensure_governance_access(
"governance_global_agents_usage",
user_id,
item_entity_type="global_agent",
item_id=str(agent.get("id") or agent.get("name") or ""),
)
elif scope_type == "group":
ensure_governance_access("governance_group_agents", user_id)
else:
ensure_governance_access("governance_user_agents", user_id)
return True
except PermissionError:
return False


def _build_authorized_discovery_candidate(
agent: Dict[str, Any],
*,
scope_type: str,
scope_id: Optional[str],
scope_name: str,
user_id: str,
) -> Optional[Dict[str, Any]]:
if not _is_agent_currently_discoverable(agent):
return None
if not _is_agent_allowed_by_governance(user_id, agent, scope_type):
return None

candidate = dict(agent)
candidate.update({
"scope_type": scope_type,
"scope_id": scope_id,
"scope_name": scope_name,
"is_global": scope_type == "global",
"is_group": scope_type == "group",
"group_id": scope_id if scope_type == "group" else None,
"group_name": scope_name if scope_type == "group" else None,
})
if scope_type == "personal":
candidate["user_id"] = user_id
candidate["catalog_key"] = build_agent_catalog_key(candidate)
return candidate if candidate["catalog_key"] else None


def build_authorized_agent_discovery_catalog(
user_id: str,
*,
settings: Optional[Dict[str, Any]] = None,
user_groups: Optional[Iterable[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
"""Build a server-only canonical catalog after current auth and policy checks."""
resolved_settings = settings or get_settings()
resolved_groups = list(user_groups) if user_groups is not None else get_user_groups(user_id)
catalog: List[Dict[str, Any]] = []

def append_candidate(agent, *, scope_type, scope_id, scope_name):
candidate = _build_authorized_discovery_candidate(
agent,
scope_type=scope_type,
scope_id=scope_id,
scope_name=scope_name,
user_id=user_id,
)
if candidate:
catalog.append(candidate)

if resolved_settings.get("allow_user_agents", False):
ensure_migration_complete(user_id)
for agent in get_personal_agents(user_id):
append_candidate(
agent,
scope_type="personal",
scope_id=user_id,
scope_name="Personal",
)

if _should_include_global_agents_for_discovery(resolved_settings):
for agent in get_global_agents():
append_candidate(
agent,
scope_type="global",
scope_id=None,
scope_name="Global",
)

if (
resolved_settings.get("enable_group_workspaces", False)
and resolved_settings.get("allow_group_agents", False)
):
for group_doc in resolved_groups:
group_id = str((group_doc or {}).get("id") or "").strip()
if not group_id:
continue
group_chat_allowed, _ = check_group_status_allows_operation(
group_doc,
"chat",
)
if not group_chat_allowed:
continue
group_name = str((group_doc or {}).get("name") or "Unnamed Group").strip()
for agent in get_group_agents(group_id):
append_candidate(
agent,
scope_type="group",
scope_id=group_id,
scope_name=group_name,
)

return catalog


def build_accessible_agent_catalog(
user_id: str,
*,
Expand Down
Loading