Skip to content
Merged
3 changes: 2 additions & 1 deletion examples/ai-file-analysis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ uv run src/main.py

## What happens to an attached file

1. `ctx.files.list()` returns the files on the incoming activity.
1. `ctx.files.list()` returns the files on the incoming activity. Images pasted into the message are listed alongside attached files, with `attachment_style` set to `"inlineImage"`. To filter by type, use filter options, e.g. `ctx.files.list(attachment_style="file")`.
2. Each file is downloaded once and reused from memory, rather than refetched through the short-lived URL.
3. `classify_file` sorts each download into `text`, `image`, or `unsupported`.
4. Unsupported files get the basic Adaptive Card. No model call is made for them.
Expand All @@ -72,6 +72,7 @@ A file arrives as metadata, not as bytes. Which route turns that metadata into b

- **A bot reads through the pre-authorized download URL.** Teams provides a URL on the attachment and the SDK fetches it directly, with no tokens attached, shown in this example.
- **An agentic user reads through Microsoft Graph.** Agentic users never receive a pre-authorized URL, so the SDK resolves the file through Graph's `/shares` endpoint using the agent's own credential.
- **A pasted image reads through the Bot API.** The SDK downloads it with the same token the app uses to reply in that conversation, not Graph. Some image formats, such as BMP and AVIF, cannot be converted by the Teams service, so Teams shows a placeholder, and they cannot be downloaded. For those, `download()` raises `InlineImageTransformationError`. Teams saves a pasted GIF as a static PNG, so it is listed as `.gif` and `image/gif` but downloads as a PNG.

### The download URL expires, and that is final

Expand Down
1 change: 1 addition & 0 deletions examples/ai-file-analysis/src/file_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def unsupported_file_card(file: IncomingFile, downloaded: DownloadedFile, note:
Fact(title="Size", value=_human_size(len(downloaded.bytes))),
Fact(title="Scope", value=file.scope),
Fact(title="Listed from", value=file.source),
Fact(title="Attachment style", value=file.attachment_style),
]
),
TextBlock(
Expand Down
14 changes: 10 additions & 4 deletions examples/ai-file-analysis/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from ai import AnalyzableFile, classify_file, is_ai_configured, prepare_analysis, run_analysis
from file_card import unsupported_file_card
from microsoft_teams.api import MessageActivity, TypingActivityInput
from microsoft_teams.apps import ActivityContext, App
from microsoft_teams.apps import ActivityContext, App, InlineImageTransformationError

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("ai-file-analysis")
Expand Down Expand Up @@ -50,9 +50,10 @@ async def handle_message(ctx: ActivityContext[MessageActivity]) -> None:
attached = await ctx.files.list()
if not attached:
await ctx.send(
"Attach one or more files. I analyze text files and images, and describe anything else I cannot read."
"Attach or paste one or more files. I analyze text files and images, and describe anything else I "
"cannot read."
if _ai_configured
else "Attach one or more files. No model is configured, so I will report what I received "
else "Attach or paste one or more files. No model is configured, so I will report what I received "
"without analyzing it."
)
return
Expand All @@ -66,7 +67,12 @@ async def handle_message(ctx: ActivityContext[MessageActivity]) -> None:
downloaded = await file.download()
except Exception as err:
logger.warning("Could not download %s: %s", file.name, err)
await ctx.send(f"I could not download {file.name}.")
await ctx.send(
"I could not download that pasted image because its format cannot be converted. "
"Try sending it as a file attachment instead."
if isinstance(err, InlineImageTransformationError)
else f"I could not download {file.name}."
)
continue

if not _ai_configured:
Expand Down
10 changes: 9 additions & 1 deletion packages/apps/src/microsoft_teams/apps/app_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
record_turn_duration,
)
from .events import ActivityEvent, ActivityResponseEvent, ActivitySentEvent, ErrorEvent
from .files_credential import select_files_credential
from .files_credential import select_files_credential, select_inline_image_credential
from .oauth_flow import OAuthFlowRegistry
from .plugins import PluginActivityEvent, PluginBase, StreamCancelledError
from .routing.activity_context import ActivityContext
Expand Down Expand Up @@ -172,6 +172,13 @@ async def _build_context(
get_agentic_graph_token=lambda identity: self.get_agentic_graph_token(identity, tenant_id),
)

# Inline images are read with the turn's Bot API token, not Graph.
files_bot_api_credential = select_inline_image_credential(
agentic_identity=activity.recipient.agentic_identity,
service_url=activity.service_url,
get_token=lambda: api_client.http.token,
)

activityCtx = ActivityContext(
activity,
self.id or "",
Expand All @@ -185,6 +192,7 @@ async def _build_context(
cloud=self.cloud,
oauth_connection_names=list(self.oauth_registry) if self.oauth_registry is not None else None,
files_credential=files_credential,
files_bot_api_credential=files_bot_api_credential,
)

send = activityCtx.send
Expand Down
7 changes: 6 additions & 1 deletion packages/apps/src/microsoft_teams/apps/files/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,24 @@
FileError,
FileScopeNotSupportedError,
FileUrlExpiredError,
InlineImageAccessError,
InlineImageTransformationError,
)
from .files_accessor import FilesAccessor
from .incoming_file import IncomingFile
from .types import FileSource
from .types import AttachmentStyle, FileSource

__all__ = [
"FileSource",
"AttachmentStyle",
"FileActor",
"FileError",
"FileAccessError",
"FileCredentialError",
"FileScopeNotSupportedError",
"FileUrlExpiredError",
"InlineImageAccessError",
"InlineImageTransformationError",
"DownloadedFile",
"IncomingFile",
"FilesAccessor",
Expand Down
Loading
Loading