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 application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.299"
VERSION = "0.261.300"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
105 changes: 95 additions & 10 deletions application/single_app/functions_m365_agent_continuation.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# functions_m365_agent_continuation.py
"""Checkpoint real agent tool history so an approval does not repeat completed calls."""

from contextlib import aclosing
from contextlib import aclosing, contextmanager
from contextvars import ContextVar
from dataclasses import asdict
from functools import wraps
Expand All @@ -14,17 +14,19 @@
from semantic_kernel.agents import ChatHistoryAgentThread
from semantic_kernel.contents import AuthorRole, ChatHistory, ChatMessageContent, FunctionCallContent, FunctionResultContent
from semantic_kernel.filters import FilterTypes
from semantic_kernel.functions import KernelArguments

Check warning on line 17 in application/single_app/functions_m365_agent_continuation.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

from functions_conversation_memory import EvidenceChunk, EvidenceSource
from functions_m365_approvals import M365ApprovalRequired, M365PolicyError
from functions_m365_execution import get_m365_execution_context
from functions_m365_execution import M365ExecutionContext, get_m365_execution_context
from m365_interaction import M365_AUTH_INTERACTION_CODES, M365SignInRequired
from functions_model_capabilities import ModelTokenBudget
from functions_model_budget_runtime import prepare_model_execution_settings


_current_journal = ContextVar("m365_agent_journal", default=None)
_current_call_id = ContextVar("m365_agent_call_id", default=None)
_current_step_model = ContextVar("m365_step_model", default=None)
_dependencies = {}
PAUSED_TOOL_MARKER = "simplechat_m365_tool_waiting"

Expand All @@ -39,15 +41,98 @@
)


def get_m365_analysis_agent(context):
class M365AnalysisModel:
"""The model that runs retained-file analysis batches for one Microsoft 365 request."""

def __init__(self, context, *, deployment_name, model_token_budget, service_and_settings):

Check warning on line 47 in application/single_app/functions_m365_agent_continuation.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
self.context = context
self.deployment_name = deployment_name
self.model_token_budget = model_token_budget

Check warning on line 50 in application/single_app/functions_m365_agent_continuation.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
self._service_and_settings = service_and_settings

async def service_and_settings(self):
"""The chat service and its execution settings; analysis removes their tools."""

Check warning on line 54 in application/single_app/functions_m365_agent_continuation.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
return await self._service_and_settings()


def _same_request(bound, context):
return (
bound.request_id == context.request_id
and bound.data_user_id == context.data_user_id
and bound.conversation_id == context.conversation_id

Check warning on line 62 in application/single_app/functions_m365_agent_continuation.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
)


def _agent_analysis_model(journal):

Check warning on line 66 in application/single_app/functions_m365_agent_continuation.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
agent = journal.agent

Check warning on line 67 in application/single_app/functions_m365_agent_continuation.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

async def service_and_settings():
return await agent._get_chat_completion_service_and_settings(

Check warning on line 70 in application/single_app/functions_m365_agent_continuation.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
kernel=agent.kernel, arguments=agent.arguments or KernelArguments(),

Check warning on line 71 in application/single_app/functions_m365_agent_continuation.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
)

return M365AnalysisModel(
journal.context,
deployment_name=getattr(agent, "deployment_name", None),

Check warning on line 76 in application/single_app/functions_m365_agent_continuation.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
model_token_budget=getattr(agent, "model_token_budget", None),
service_and_settings=service_and_settings,
)


def get_m365_analysis_model(context):
"""The model that runs retained-file analysis for this exact Microsoft 365 request.

In chat it is the selected agent, through its continuation journal. An orchestration
action step has no agent, so it is the step's own model, bound by
``m365_step_model_binder``. Neither serves another request, data user or conversation.
"""
journal = _current_journal.get()
if journal is None or (
journal.context.request_id != context.request_id
or journal.context.data_user_id != context.data_user_id
or journal.context.conversation_id != context.conversation_id
):
raise M365PolicyError("m365_analysis_unavailable", "A selected conversation agent is required for deeper analysis.")
return journal.agent
if journal is not None and _same_request(journal.context, context):
return _agent_analysis_model(journal)
step_model = _current_step_model.get()
if step_model is not None and _same_request(step_model.context, context):
return step_model
raise M365PolicyError("m365_analysis_unavailable", "A selected conversation agent is required for deeper analysis.")


def m365_step_model_binder(context, *, service, model_token_budget, tool_schemas=()):
"""Validate an orchestration step's own model once, and return its per-call binder.

A chat agent's continuation journal binds its model's token budget before each tool
call, and offers the agent to retained-file analysis. An orchestration action step
calls its model without an agent, so file reads were refused with
``model_context_unavailable`` and analysis with ``m365_analysis_unavailable``. The
binder sets both for one function call, for the step's own request only.
"""
setter = _dependencies.get("model_context_setter")
reset = _dependencies.get("model_context_reset")
if setter is None or reset is None:
raise M365PolicyError("m365_continuation_unavailable", "Durable agent continuation is not configured.")
if not isinstance(context, M365ExecutionContext) or not isinstance(model_token_budget, ModelTokenBudget):
raise M365PolicyError("m365_analysis_unavailable", "The step's model is not bound to this Microsoft 365 request.")
schemas = list(tool_schemas)

async def service_and_settings():
return service, service.get_prompt_execution_settings_class()(service_id=service.service_id)

analysis_model = M365AnalysisModel(
context,
deployment_name=getattr(service, "ai_model_id", None),
model_token_budget=model_token_budget,
service_and_settings=service_and_settings,
)

@contextmanager
def bind(messages):
budget_token = setter(model_token_budget, list(messages or ()), tool_schemas=schemas)
model_token = _current_step_model.set(analysis_model)
try:
yield
finally:
_current_step_model.reset(model_token)
reset(budget_token)

return bind


def _approval_error(error):
Expand Down
13 changes: 5 additions & 8 deletions application/single_app/functions_m365_analysis_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
import re

from semantic_kernel.contents import ChatHistory
from semantic_kernel.functions import KernelArguments

from conversation_memory_runtime import resolve_m365_memory
from functions_m365_agent_continuation import get_m365_analysis_agent
from functions_m365_agent_continuation import get_m365_analysis_model
from functions_m365_analysis_jobs import AnalysisBatchResult, ConversationAnalysisJobRunner
from functions_m365_approvals import M365PolicyError, get_m365_approval_service
from functions_m365_execution import authorize_m365_publication
Expand Down Expand Up @@ -63,9 +62,9 @@ async def analyze_m365_memory(context, source, action_id, memory_id, question, a
"message": "Use the available excerpts; deeper file analysis was not approved.",
"coverage": {"complete": False},
}
agent = get_m365_analysis_agent(context)
model = getattr(agent, "deployment_name", None)
model_budget = resolve_model_token_budget(getattr(agent, "model_token_budget", None) or model)
analysis_model = get_m365_analysis_model(context)
model = analysis_model.deployment_name
model_budget = resolve_model_token_budget(analysis_model.model_token_budget or model)
processor_version = "m365-v1-" + hashlib.sha256(
json.dumps({"model": model, "budget": asdict(model_budget), "question": question}, sort_keys=True).encode("utf-8")
).hexdigest()[:24]
Expand All @@ -86,9 +85,7 @@ async def generate(batch):
"evidence": batch.evidence,
}, ensure_ascii=False)
history.add_user_message(content)
service, settings = await agent._get_chat_completion_service_and_settings(
kernel=agent.kernel, arguments=agent.arguments or KernelArguments(),
)
service, settings = await analysis_model.service_and_settings()
try:
settings, batch_budget = prepare_model_execution_settings(
settings, model_budget, output_limit=1536,
Expand Down
120 changes: 109 additions & 11 deletions application/single_app/functions_orchestration_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
kernel holds only the built-in chart tools, never the action's own functions, so saved
visual preferences can be applied there without reaching calls to the integration.

Version: 0.261.238
Version: 0.261.300
Microsoft 365 actions authorized for their own step in: 0.261.238
SharePoint and OneDrive steps bind their model's token budget and analysis model in: 0.261.300
"""

import asyncio
Expand Down Expand Up @@ -39,7 +40,10 @@
from functions_orchestration_invocation_capture import require_invocation_capture
from functions_orchestration_m365 import (
action_step_scope,
file_step_filter,
file_step_model_limits,
is_m365_action_manifest,
m365_file_source,
result_refusal,
step_error,
)
Expand Down Expand Up @@ -94,7 +98,12 @@ def _check_access(settings, catalog, action_ref):
return selected


def _build_action_model(settings, context, user_id, *, capture_configuration=False):
def _resolve_action_model(settings, context, user_id):
"""Resolve and build the step's authorized model once.

Returns its service, the protocol the service speaks, the resolved endpoint record (None
for the classic deployment settings) and the model context.
"""
# Model and plugin dependencies initialize clients; import them only for a running step.
from functions_model_endpoint_runtime import (
build_semantic_kernel_chat_service_for_model,
Expand Down Expand Up @@ -137,9 +146,10 @@ def _build_action_model(settings, context, user_id, *, capture_configuration=Fal
deployment, settings, service_id='orchestration-action',
model_context=model_context, resolved_model_endpoint=endpoint,
)
if not capture_configuration:
return service
return service, protocol, endpoint, model_context


def _action_model_configuration(settings, service, protocol, endpoint, model_context):
connection = (endpoint or {}).get('connection') or {}
if endpoint:
configured_endpoint = connection.get('endpoint')
Expand All @@ -148,14 +158,75 @@ def _build_action_model(settings, context, user_id, *, capture_configuration=Fal
prefix = 'azure_apim_gpt' if settings.get('enable_gpt_apim') else 'azure_openai_gpt'
configured_endpoint = settings.get(f'{prefix}_endpoint')
configured_version = settings.get(f'{prefix}_api_version')
return service, azure_chat_construction_metadata(
return azure_chat_construction_metadata(
service, protocol=protocol,
provider=(endpoint.get('provider') or 'aoai') if endpoint else 'aoai',
configured_endpoint=configured_endpoint, configured_api_version=configured_version,
endpoint_id=(endpoint or {}).get('id'), model_id=model_context.get('model_id'),
)


def _build_action_model(settings, context, user_id, *, capture_configuration=False):
service, protocol, endpoint, model_context = _resolve_action_model(settings, context, user_id)
if not capture_configuration:
return service
return service, _action_model_configuration(settings, service, protocol, endpoint, model_context)


def _action_model_budget(settings, service, protocol, endpoint, model_context):
"""The step model's token budget, from the same records its service was built from.

Mirrors ``build_agent_model_budget`` with secret-free projections of those records. The
step does not cap its model's output, so no request limit is set: the model's documented
output limit is reserved instead. Raises ``ModelTokenBudgetError`` for invalid limits.
"""
# Budget and endpoint metadata helpers are only needed by a running file step.
from functions_model_capabilities import project_model_budget_metadata, resolve_model_token_budget
from functions_model_endpoint_providers import MODEL_ENDPOINT_PROTOCOL_ANTHROPIC
from functions_model_endpoint_types import get_model_endpoint_api_type, resolve_model_endpoint_request_model

deployment = getattr(service, 'ai_model_id', None) or model_context.get('model_deployment')
if endpoint:
model_id = str(model_context.get('model_id') or '').strip()
models = [model for model in endpoint.get('models') or [] if isinstance(model, dict)]
record = next(
(model for model in models if model_id and str(model.get('id') or '').strip() == model_id), None,
) or next(
(model for model in models if resolve_model_endpoint_request_model(endpoint, model) == deployment),
None,
)
endpoint_metadata = project_model_budget_metadata(endpoint)
provider = endpoint.get('provider') or 'aoai'
if get_model_endpoint_api_type(endpoint) == 'azure_openai':
provider = 'azure'
else:
record = next((
model for model in (settings.get('gpt_model') or {}).get('selected') or []
if isinstance(model, dict) and model.get('deploymentName') == deployment
), None)
endpoint_metadata = {'provider': 'aoai'}
provider = 'aoai'
return resolve_model_token_budget(
project_model_budget_metadata(record) if record else {'deploymentName': deployment},
endpoint_metadata, provider=provider,
protocol='messages' if protocol == MODEL_ENDPOINT_PROTOCOL_ANTHROPIC else 'chat_completions',
)


def _build_file_action_model(settings, context, user_id, *, capture_configuration=False):
"""A SharePoint or OneDrive step's model, its capture configuration, and its token budget.

File functions bound the content they return by the model's verified token limits, so a
file step resolves the budget from the same single resolution as its service.
"""
service, protocol, endpoint, model_context = _resolve_action_model(settings, context, user_id)
configuration = (
_action_model_configuration(settings, service, protocol, endpoint, model_context)
if capture_configuration else None
)
return service, configuration, _action_model_budget(settings, service, protocol, endpoint, model_context)


def _model_usage(messages):
usage = {}
seen = set()
Expand Down Expand Up @@ -450,7 +521,9 @@ async def invoke_action(
invocation capture it acquires nothing the capture has not already attested.

A Microsoft 365 action runs inside its own step's Microsoft 365 context, keyed by
``m365_request_key`` so a retry of the step reuses its request and any approval.
``m365_request_key`` so a retry of the step reuses its request and any approval. A
SharePoint or OneDrive action also binds the step model's token budget around each of
its function calls, and offers that model to deeper file analysis, as a chat agent does.
"""
# Keep the planner/registry importable without SK and the Azure application bootstrap.
from semantic_kernel import Kernel
Expand Down Expand Up @@ -516,9 +589,13 @@ async def invoke_action(
current_settings = deepcopy(current_settings)
manifest = deepcopy(manifest)
m365_step = is_m365_action_manifest(manifest)
# SharePoint and OneDrive functions also need the step model's token budget.
file_source = m365_file_source(manifest)
m365_context = None
model_budget = None
if m365_step:
# Sign-in is checked here, before the model or Microsoft Graph is called.
m365_scope.enter_context(action_step_scope(
m365_context = m365_scope.enter_context(action_step_scope(
action_ref, user_id=user_id, conversation_id=getattr(context, 'conversation_id', None),
request_key=m365_request_key, user_groups=getattr(context, 'active_group_ids', None),
origin=m365_origin,
Expand Down Expand Up @@ -611,9 +688,15 @@ async def stop_after_failure(invocation, next):
try:
prepared = prepare_action_plugin_manifest(manifest, current_settings)
if invocation_capture is not None:
service, model_configuration = _build_action_model(
current_settings, context, user_id, capture_configuration=True,
)
if file_source:
with file_step_model_limits(file_source):
service, model_configuration, model_budget = _build_file_action_model(
current_settings, context, user_id, capture_configuration=True,
)
else:
service, model_configuration = _build_action_model(
current_settings, context, user_id, capture_configuration=True,
)
kernel.add_service(service)
if model_configuration is None:
invocation_capture.refuse()
Expand All @@ -635,10 +718,25 @@ async def stop_after_failure(invocation, next):
kernel.add_filter(FilterTypes.FUNCTION_INVOCATION, guard_function)
kernel.add_filter(FilterTypes.AUTO_FUNCTION_INVOCATION, stop_after_failure)
if invocation_capture is None:
service = _build_action_model(current_settings, context, user_id)
if file_source:
with file_step_model_limits(file_source):
service, _configuration, model_budget = _build_file_action_model(
current_settings, context, user_id,
)
else:
service = _build_action_model(current_settings, context, user_id)
kernel.add_service(service)
if not getattr(service, 'SUPPORTS_FUNCTION_CALLING', False):
raise ActionExecutionError('The selected model does not support action functions.')
if file_source:
# Chat binds these through the selected agent; this step's own model stands in.
kernel.add_filter(FilterTypes.AUTO_FUNCTION_INVOCATION, file_step_filter(
m365_context, service=service, model_token_budget=model_budget, source=file_source,
tool_schemas=[
metadata.model_dump(mode='json', exclude_none=True)
for metadata in kernel.get_full_list_of_function_metadata()
],
))
system_prompt = (
'Complete one knowledge-collection step using only the supplied action functions. '
'Use the action descriptions to choose functions and arguments. You may make '
Expand Down
Loading
Loading