Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
18 changes: 17 additions & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,23 @@ Agents can receive and process notifications from Agent 365:
- **WPX Comments**: Handle Word document comments
- **Custom Notifications**: Extensible notification types

### 7. Graceful Degradation
### 7. User Identity

The A365 platform populates `Activity.From` on every incoming message with the user's basic
information — no API calls or token acquisition required:

| Field | C# | Python | TypeScript |
|---|---|---|---|
| Channel user ID | `Activity.From.Id` | `activity.from_property.id` | `activity.from.id` |
| Display name | `Activity.From.Name` | `activity.from_property.name` | `activity.from.name` |
| Azure AD Object ID | `Activity.From.AadObjectId` | `activity.from_property.aad_object_id` | `activity.from.aadObjectId` |

**Pattern applied in every sample:**
1. Log all three fields at `Information`/`info` level at message handler entry
2. Inject the display name into LLM system instructions for personalized responses
3. Use `AadObjectId` to call Microsoft Graph for extended profile data when needed

### 8. Graceful Degradation

All samples support graceful degradation when tools fail:

Expand Down
244 changes: 244 additions & 0 deletions docs/user-identity-rollout-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
# User Identity Rollout — Implementation Plan
Comment thread
sellakumaran marked this conversation as resolved.
Outdated

## Context

Agents need to identify who they are talking to in order to personalize responses and log user activity. The universal pattern across all 15 samples is:

1. Log `Activity.From` fields (`Name`, `Id`, `AadObjectId`) at message handler entry
2. Inject the user's display name into the LLM system instructions / prompt
3. Document the pattern in the sample README

No new tool files, no Graph calls, no per-orchestrator tool registration.

The `dotnet/agent-framework` sample was originally built with a `CurrentUserTool` (LLM-callable tools + Graph `/me`). That is being simplified to match the uniform pattern used by all other samples.

---

## Workflow

For `dotnet/agent-framework` (Step 0):
1. Make changes locally
2. User deploys and tests manually
3. Commit and push
4. Proceed with remaining samples

---

## Step 0 — Simplify `dotnet/agent-framework` (branch `users/sellak/user-identity`)

**Delete:** `dotnet/agent-framework/sample-agent/Tools/CurrentUserTool.cs`

**Modify `Agent/MyAgent.cs`:**
- Remove the `CurrentUserTool` instantiation and the two `AIFunctionFactory.Create(currentUserTool.*)` registrations
- Remove the Graph-related instruction from `AgentInstructionsTemplate`:
```
For richer user profile information (email, job title, department), use {{CurrentUserTool.GetCurrentUserExtendedProfileAsync}}.
```
- Keep: `accessToken` acquisition, `agentId` warning log, logging of `Activity.From`, `{userName}` injection into instructions

**Modify `README.md`:**
- Simplify "Working with User Identity" to the activity-payload pattern only (field table + log snippet)
- Remove "Extended profile from Microsoft Graph" subsection
- Remove `Tools/CurrentUserTool.cs` file reference

**Verify:** `dotnet build` passes, no `CurrentUserTool` references remain.

---

## Changes Per Sample (Steps 1–9, after Step 0 is tested and committed)

Every sample gets the same three changes — no new files:

| # | Change | Detail |
|---|--------|--------|
| 1 | **Log** | Add structured log of `Activity.From` (Name, Id, AadObjectId) at the start of the message handler |
| 2 | **Inject** | Read `Activity.From.Name` and inject it into the LLM system instructions / prompt template as the user's display name |
| 3 | **README** | Add "Working with User Identity" section (see template below) |

---

### C# — `dotnet/semantic-kernel/sample-agent/`

**Modify `Agents/MyAgent.cs`:**

```csharp
var fromAccount = turnContext.Activity.From;
_logger?.LogInformation(
"Turn received from user — DisplayName: '{Name}', UserId: '{Id}', AadObjectId: '{AadObjectId}'",
fromAccount?.Name ?? "(unknown)",
fromAccount?.Id ?? "(unknown)",
fromAccount?.AadObjectId ?? "(none)");
```

Inject `Activity.From.Name` into the SK kernel's system instructions using the same `{userName}` template replacement pattern as the reference.

**Modify `README.md`** — add section.

---

### Python — 5 samples

Python uses `activity.from_property`. Samples with `turn_context_utils.py` (`claude`, `crewai`) already extract `caller_name`, `caller_id`, `caller_aad_object_id` — reuse those.

**Modify `agent.py` in each sample:**

```python
from_prop = turn_context.activity.from_property
logger.info(
"Turn received from user — DisplayName: '%s', UserId: '%s', AadObjectId: '%s'",
getattr(from_prop, "name", None) or "(unknown)",
getattr(from_prop, "id", None) or "(unknown)",
getattr(from_prop, "aad_object_id", None) or "(none)",
)
display_name = getattr(from_prop, "name", None) or "unknown"
```

Inject `display_name` into the system prompt string.

| Sample | Notes |
|--------|-------|
| `python/agent-framework/sample-agent/` | No existing identity code. Add logging + injection. |
| `python/claude/sample-agent/` | Has `turn_context_utils.py` — reuse `caller_details`. |
| `python/openai/sample-agent/` | No existing identity code. Add logging + injection. |
| `python/crewai/sample_agent/` | Has `turn_context_utils.py` — reuse `caller_details`. |
| `python/google-adk/sample-agent/` | No existing identity code. Add logging + injection. |

**Modify `README.md`** in each — add section.

---

### Node.js / TypeScript — 8 samples

**Modify `src/agent.ts` in each sample (except n8n):**

```typescript
const from = turnContext.activity?.from;
logger.info(
`Turn received from user — DisplayName: '${from?.name ?? "(unknown)"}', UserId: '${from?.id ?? "(unknown)"}', AadObjectId: '${from?.aadObjectId ?? "(none)"}'`
);
const displayName = from?.name ?? "unknown";
```

Inject `displayName` into the system prompt string.

| Sample | Notes |
|--------|-------|
| `nodejs/openai/sample-agent/` | Add logging + injection. |
| `nodejs/claude/sample-agent/` | Add logging + injection. |
| `nodejs/langchain/sample-agent/` | Add logging + injection. |
| `nodejs/devin/sample-agent/` | Explore structure first, then apply. |
| `nodejs/n8n/` | **README only** — no agent code to modify. |
| `nodejs/perplexity/sample-agent/` | Already extracts userId/userName/aadObjectId — add log line + inject into prompt. |
| `nodejs/vercel-sdk/sample-agent/` | Add logging + injection. |
| `nodejs/copilot-studio/sample-agent/` | Explore structure first, then apply. |

**Modify `README.md`** in each — add section.

---

## Design Doc Updates (4 files)

Prose additions only — no code changes.

| File | Change |
|------|--------|
| `docs/design.md` | Add "User Identity" note under Message Processing Flow |
| `dotnet/docs/design.md` | Add C# logging snippet + name-injection pattern |
| `python/docs/design.md` | Add Python snippet using `activity.from_property` |
| `nodejs/docs/design.md` | Add TypeScript snippet using `activity?.from` |

Content for all four:
> Agents identify the user from `Activity.From` (populated by the A365 platform on every message). Log `Id`, `Name`, and `AadObjectId` at `Information`/`info` level at message handler entry. Inject `Name` into LLM system instructions for personalization. For extended profile data (email, job title), a delegated Graph call to `/me` is required (app-only tokens use `/users/{AadObjectId}`).

---

## README "Working with User Identity" Template

Use this in every sample README:

```markdown
## Working with User Identity

On every incoming message, the A365 platform populates `Activity.From` with basic user
information — always available with no API calls or token acquisition:

| Field | Description |
|---|---|
| `Activity.From.Id` | Channel-specific user ID (e.g., `29:1AbcXyz...` in Teams) |
| `Activity.From.Name` | Display name as known to the channel |
| `Activity.From.AadObjectId` | Azure AD Object ID — use this to call Microsoft Graph |

The sample logs these fields at the start of every message turn and injects the display name
into the LLM system instructions for personalized responses.
```

---

## Execution Order

0. **`dotnet/agent-framework`** — simplify locally → user tests → commit and push
1. **`dotnet/semantic-kernel`** — closest to reference; validates C# pattern in SK context
2. **`python/claude`**, **`python/crewai`** — already have `turn_context_utils`; quick wins
3. **`python/agent-framework`**, **`python/openai`**, **`python/google-adk`**
4. **`nodejs/perplexity`** — already extracts identity; log + inject + README
5. **`nodejs/openai`**, **`nodejs/claude`**, **`nodejs/langchain`**, **`nodejs/vercel-sdk`**
6. **`nodejs/devin`**, **`nodejs/copilot-studio`** — explore structure before modifying
7. **`nodejs/n8n`** — README only
8. **Design docs** (all 4)

---

## Testing Strategy

### Tier 1 — Build/compile gate (all samples, required)

| Language | Command | Run from |
|----------|---------|---------|
| C# | `dotnet build` | solution directory |
| Python | `python -m py_compile agent.py` | `sample-agent/` |
| TypeScript | `npm run build` | `sample-agent/` |

n8n is README-only — no build step.

### Tier 2 — `/review-staged` gate (all samples, required)

Run `/review-staged` after implementing each sample batch, before committing. All critical/high findings must be resolved.

### Tier 3 — E2E functional test (one per language, required)

`dotnet/agent-framework` is the E2E baseline (user-deployed and tested). Additionally validate one per language:

| Language | Sample | What to verify |
|----------|--------|----------------|
| C# | `dotnet/semantic-kernel` | Send a message — LLM response uses the user's name |
| Python | `python/agent-framework` | Send a message — LLM response uses the display name |
| Node.js | `nodejs/openai` | Send a message — LLM response uses the display name |

The remaining 11 samples are covered by identical pattern + build pass + `/review-staged`.

### Tier 4 — Design docs (review only)

Prose-only additions — verify accuracy against reference code, no deploy needed.

---

## Public Documentation Suggestions

**Target page:** [a365-dev-lifecycle — Step 1: Build and run agent](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/a365-dev-lifecycle#1-build-and-run-agent)

Step 1 currently lists: Observability, Notifications, Tooling, Agent Identity — no mention of identifying the human user on each message. Suggested addition (file as a docs PR against `MicrosoftDocs/agent365-docs-pr`):

**New bullet after "Agent Identity":**

```markdown
- **User Identity** – On every incoming message the A365 platform populates `Activity.From`
with the user's display name, channel user ID, and Azure AD Object ID. No additional API call
is required. For extended profile data (email, job title, department), call Microsoft Graph
`/me` using the access token already acquired for the turn (delegated token with `User.Read`
scope; use `/users/{AadObjectId}` for app-only tokens).
```

**New linked page `user-identity.md`** covering:
- The three-field table (`Activity.From.Id`, `.Name`, `.AadObjectId`)
- When to use `Activity.From` vs Graph `/me`
- Code snippet links to the Agent365-Samples repository
Loading
Loading