Add user identity pattern to dotnet/agent-framework sample#233
Add user identity pattern to dotnet/agent-framework sample#233sellakumaran wants to merge 2 commits intomainfrom
Conversation
Demonstrates how A365 agents identify the user they are talking to: - Log Activity.From (Id, Name, AadObjectId) at the start of each turn - CurrentUserTool exposes GetCurrentUser() (activity payload, no API call) and GetCurrentUserExtendedProfileAsync() (Microsoft Graph /me) as LLM-callable tools - Inject user's display name into agent instructions for personalized responses - README section explains both patterns and token requirements for Graph calls Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Pull request overview
Adds a “user identity” pattern to the .NET Agent Framework sample, showing how to identify the current user from the activity payload and (optionally) enrich that identity via Microsoft Graph, plus using the user’s name for more personalized agent behavior.
Changes:
- Introduces
CurrentUserToolexposing LLM-callable functions for basic identity (Activity.From) and extended profile (GET /mevia Graph). - Logs
Activity.Fromidentity fields at the start of each turn and injects the user display name into agent instructions/welcome message. - Documents both identity patterns and token requirements in the sample README.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| dotnet/agent-framework/sample-agent/Tools/CurrentUserTool.cs | Adds new tool for retrieving basic identity and (optionally) Graph /me profile. |
| dotnet/agent-framework/sample-agent/Agent/MyAgent.cs | Logs user identity each turn, registers the new tool, and personalizes instructions/welcome text using user display name. |
| dotnet/agent-framework/sample-agent/README.md | Documents basic identity vs. Graph-based extended profile and token requirements. |
You can also share your feedback on Copilot code review. Take the survey.
| private static string GetAgentInstructions(string? userName) => | ||
| AgentInstructionsTemplate.Replace("{userName}", string.IsNullOrEmpty(userName) ? "unknown" : userName, StringComparison.Ordinal); |
There was a problem hiding this comment.
displayName comes from Activity.From.Name (channel-provided) and is inserted verbatim into the system instructions. Because this is untrusted user-controlled text, it can enable prompt-injection (e.g., a display name containing newlines/instructions) or unexpectedly bloat the prompt. Consider sanitizing/normalizing the name (strip control chars/newlines, trim, enforce a max length) and/or explicitly marking it as untrusted text in the instructions (e.g., quote it and instruct the model not to treat it as instructions).
| _logger?.LogInformation( | ||
| "Turn received from user — DisplayName: '{Name}', UserId: '{Id}', AadObjectId: '{AadObjectId}'", | ||
| fromAccount?.Name ?? "(unknown)", | ||
| fromAccount?.Id ?? "(unknown)", | ||
| fromAccount?.AadObjectId ?? "(none)"); |
There was a problem hiding this comment.
This logs DisplayName, channel user ID, and especially AadObjectId at Information level on every turn. Those values are user identifiers/PII and will commonly end up in production logs. Consider reducing this to Debug (or feature-flagging it), and/or redacting/hashing AadObjectId before logging.
| // Note: In production, inject IHttpClientFactory via the constructor to avoid socket exhaustion. | ||
| using var httpClient = new HttpClient(); | ||
| httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); | ||
|
|
||
| var response = await httpClient.GetAsync(graphEndpoint).ConfigureAwait(false); |
There was a problem hiding this comment.
GetCurrentUserExtendedProfileAsync creates and disposes a new HttpClient per call. Even in samples this can lead to socket exhaustion and makes it harder to apply retry/timeouts consistently. Prefer injecting IHttpClientFactory (e.g., a named/typed Graph client) and reusing it here, with an explicit timeout and any existing resilience policies.
| var parsed = JsonSerializer.Deserialize<JsonElement>(json); | ||
| return JsonSerializer.Serialize(parsed, new JsonSerializerOptions { WriteIndented = true }); |
There was a problem hiding this comment.
JsonSerializer.Deserialize<JsonElement>(json) will throw if Graph returns non-JSON (or unexpected JSON) even on a 2xx response, which would surface as a tool failure. Consider wrapping the parse/pretty-print in a try/catch for JsonException and returning a safe fallback (e.g., raw response text or a clearer error) instead of throwing.
| var parsed = JsonSerializer.Deserialize<JsonElement>(json); | |
| return JsonSerializer.Serialize(parsed, new JsonSerializerOptions { WriteIndented = true }); | |
| try | |
| { | |
| var parsed = JsonSerializer.Deserialize<JsonElement>(json); | |
| return JsonSerializer.Serialize(parsed, new JsonSerializerOptions { WriteIndented = true }); | |
| } | |
| catch (JsonException ex) | |
| { | |
| logger?.LogWarning(ex, "Failed to parse Graph /me response as JSON. Returning raw response text."); | |
| return json; | |
| } |
Activity.From.Name is not reliably populated in the MembersAdded event in Teams/A365 flows. The user name is injected into the LLM instructions on the first message turn instead, where Activity.From.Name is always set. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Demonstrates how A365 agents identify the user they are talking to: