Skip to content

Commit 2fbdb06

Browse files
authored
Merge pull request #1019 from vgrem/feat/teams-presence-example
feat: add Teams presence and telephony status example
2 parents d9af073 + ea349c5 commit 2fbdb06

1 file changed

Lines changed: 230 additions & 0 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
"""
2+
Teams presence and telephony status monitor — practical real-world example.
3+
4+
A call center / helpdesk scenario: monitor agent availability across a team,
5+
detect state changes, bulk-query presence, and respond programmatically.
6+
7+
Covers:
8+
- Getting a single user's presence
9+
- Getting presence for multiple users at once (bulk)
10+
- Setting and clearing preferred presence
11+
- Setting a status message with expiry
12+
- Polling for state changes
13+
- Mapping presence states to business rules
14+
15+
Required permissions:
16+
- Presence.Read (delegated) — read own presence
17+
- Presence.Read.All (application) — read any user's presence
18+
- Presence.ReadWrite (delegated) — set own presence + status message
19+
20+
See https://learn.microsoft.com/en-us/graph/api/resources/presence
21+
"""
22+
23+
from __future__ import annotations
24+
25+
from datetime import datetime, timedelta, timezone
26+
from time import sleep
27+
from typing import Dict, List, Optional
28+
29+
from office365.graph_client import GraphClient
30+
31+
32+
# ---------------------------------------------------------------------------
33+
# Configuration
34+
# ---------------------------------------------------------------------------
35+
36+
SUPPORTED_AGENTS = [
37+
"alice@company.com",
38+
"bob@company.com",
39+
"carol@company.com",
40+
]
41+
42+
POLL_INTERVAL_SECONDS = 30
43+
BUSY_ACTIVITIES = {"InACall", "InAConferenceCall", "InAMeeting", "Presenting"}
44+
45+
46+
# ---------------------------------------------------------------------------
47+
# Business logic helpers
48+
# ---------------------------------------------------------------------------
49+
50+
def is_available(presence) -> bool:
51+
"""Determine if a user is considered 'available' for routing."""
52+
availability = presence.get_property("availability")
53+
activity = presence.get_property("activity")
54+
if not availability or not activity:
55+
return False
56+
if availability in ("DoNotDisturb", "Offline", "PresenceUnknown"):
57+
return False
58+
if activity in BUSY_ACTIVITIES | {"OffWork", "OutOfOffice"}:
59+
return False
60+
return True
61+
62+
63+
def agent_status_summary(presence) -> str:
64+
"""Human-readable summary of an agent's current state."""
65+
availability = presence.get_property("availability") or "Unknown"
66+
activity = presence.get_property("activity") or "Unknown"
67+
return f"availability={availability}, activity={activity}"
68+
69+
70+
# ---------------------------------------------------------------------------
71+
# Core operations
72+
# ---------------------------------------------------------------------------
73+
74+
def get_presence_for_user(client: GraphClient, user_id: str):
75+
"""Get presence for a single user by ID or UPN."""
76+
return client.communications.presences[user_id].get().execute_query()
77+
78+
79+
def get_presences_for_team(client: GraphClient, user_ids: List[str]):
80+
"""Get presence for multiple users in a single API call (bulk)."""
81+
return client.communications.get_presences_by_user_id(user_ids).execute_query()
82+
83+
84+
def take_agent_offline(client: GraphClient, user_id: str):
85+
"""Mark an agent as DoNotDisturb + set an end-of-day status message."""
86+
expires = datetime.now(timezone.utc) + timedelta(hours=9)
87+
88+
client.communications.presences[user_id].set_user_preferred_presence(
89+
availability="DoNotDisturb",
90+
activity="OffWork",
91+
expiration_duration="PT9H",
92+
).execute_query()
93+
94+
client.communications.presences[user_id].set_status_message(
95+
message="Shift ended — back tomorrow",
96+
expiry=expires,
97+
).execute_query()
98+
99+
100+
def set_agent_available(client: GraphClient, user_id: str):
101+
"""Return an agent to available state after break."""
102+
client.communications.presences[user_id].set_user_preferred_presence(
103+
availability="Available",
104+
activity="Available",
105+
expiration_duration="PT8H",
106+
).execute_query()
107+
108+
109+
# ---------------------------------------------------------------------------
110+
# Scenario 1: snapshot — check current team status
111+
# ---------------------------------------------------------------------------
112+
113+
def snapshot_team_status(client: GraphClient, agents: List[str]):
114+
"""One-shot display of every agent's current presence."""
115+
print("=== Team presence snapshot ===")
116+
presences = get_presences_for_team(client, agents)
117+
for entry in presences:
118+
print(f" {entry.id:40s} {agent_status_summary(entry)}")
119+
print()
120+
121+
122+
# ---------------------------------------------------------------------------
123+
# Scenario 2: monitor — poll for state changes
124+
# ---------------------------------------------------------------------------
125+
126+
def monitor_team_status(client: GraphClient, agents: List[str], cycles: int = 5):
127+
"""Poll presence for a team and report state changes."""
128+
print(f"=== Monitoring {len(agents)} agents (poll every {POLL_INTERVAL_SECONDS}s) ===")
129+
130+
previous: Dict[str, str] = {}
131+
132+
for cycle in range(1, cycles + 1):
133+
print(f"\n--- Cycle {cycle}/{cycles} [{datetime.now(timezone.utc).isoformat()}] ---")
134+
presences = get_presences_for_team(client, agents)
135+
136+
for entry in presences:
137+
user_id = entry.id
138+
state = agent_status_summary(entry)
139+
old_state = previous.get(user_id)
140+
141+
if old_state is None:
142+
print(f" INIT {user_id:35s} {state}")
143+
elif state != old_state:
144+
print(f" >>> {user_id:35s} {old_state} -> {state}")
145+
if is_available(entry) and not is_available.__doc__:
146+
print(f" Agent {user_id} is now available for routing")
147+
else:
148+
print(f" . {user_id:35s} {state}")
149+
150+
previous[user_id] = state
151+
152+
if cycle < cycles:
153+
sleep(POLL_INTERVAL_SECONDS)
154+
155+
print()
156+
157+
158+
# ---------------------------------------------------------------------------
159+
# Scenario 3: round-robin availability check for routing
160+
# ---------------------------------------------------------------------------
161+
162+
def find_available_agent(client: GraphClient, agents: List[str]) -> Optional[str]:
163+
"""Return the first available agent (useful for automatic call routing)."""
164+
presences = get_presences_for_team(client, agents)
165+
for entry in presences:
166+
if is_available(entry):
167+
return entry.id
168+
return None
169+
170+
171+
# ---------------------------------------------------------------------------
172+
# Scenario 4: set status message on shift start
173+
# ---------------------------------------------------------------------------
174+
175+
def start_shift(client: GraphClient, user_id: str, agent_name: str):
176+
"""Begin shift: set available presence with a status message."""
177+
expires = datetime.now(timezone.utc) + timedelta(hours=9)
178+
client.communications.presences[user_id].set_user_preferred_presence(
179+
availability="Available",
180+
activity="Available",
181+
expiration_duration="PT9H",
182+
).execute_query()
183+
184+
client.communications.presences[user_id].set_status_message(
185+
message=f"{agent_name} is online — ready to take calls",
186+
expiry=expires,
187+
).execute_query()
188+
189+
190+
# ---------------------------------------------------------------------------
191+
# Main — run all scenarios
192+
# ---------------------------------------------------------------------------
193+
194+
if __name__ == "__main__":
195+
# Connect (choose one auth method)
196+
#
197+
# Option A: certificate
198+
# client = GraphClient(tenant).with_client_certificate(client_id, thumbprint, key)
199+
#
200+
# Option B: client secret
201+
# client = GraphClient(tenant).with_client_secret(client_id, client_secret)
202+
#
203+
# Option C: device code (interactive, works with MFA)
204+
client = GraphClient(tenant).with_device_flow(client_id="your_client_id")
205+
# (For brevity replace with real credentials above)
206+
207+
# ------------------------------------------------------------------
208+
# Scenario 1 — read-only snapshot
209+
# ------------------------------------------------------------------
210+
snapshot_team_status(client, SUPPORTED_AGENTS)
211+
212+
# ------------------------------------------------------------------
213+
# Scenario 2 — find someone to route a call to
214+
# ------------------------------------------------------------------
215+
available = find_available_agent(client, SUPPORTED_AGENTS)
216+
if available:
217+
print(f"Routing call to: {available}")
218+
else:
219+
print("No agents available — queuing call")
220+
221+
# ------------------------------------------------------------------
222+
# Scenario 3 — monitor for state changes (run 3 cycles)
223+
# ------------------------------------------------------------------
224+
monitor_team_status(client, SUPPORTED_AGENTS, cycles=3)
225+
226+
# ------------------------------------------------------------------
227+
# Scenario 4 — mark agent as off (end of shift)
228+
# ------------------------------------------------------------------
229+
take_agent_offline(client, SUPPORTED_AGENTS[0])
230+
print(f"Agent {SUPPORTED_AGENTS[0]} marked as offline")

0 commit comments

Comments
 (0)