Skip to content

Commit ec6160d

Browse files
committed
refactor: strip redundant wrappers from presence monitor example
1 parent da77814 commit ec6160d

30 files changed

Lines changed: 234 additions & 383 deletions
Lines changed: 49 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,23 @@
11
"""
2-
Teams presence and telephony status monitor — practical real-world example.
2+
Teams presence and telephony status monitor.
33
4-
A call center / helpdesk scenario: monitor agent availability across a team,
5-
detect state changes, bulk-query presence, and respond programmatically.
4+
Covers: single/bulk presence queries, preferred presence, status messages,
5+
polling for state changes, and business-rules routing logic.
66
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
7+
Required permissions: Presence.Read, Presence.Read.All, Presence.ReadWrite.
218
"""
229

23-
from __future__ import annotations
24-
2510
from datetime import datetime, timedelta, timezone
2611
from time import sleep
27-
from typing import Dict, List, Optional
12+
from typing import Optional
2813

2914
from office365.graph_client import GraphClient
3015

31-
# ---------------------------------------------------------------------------
32-
# Configuration
33-
# ---------------------------------------------------------------------------
34-
35-
SUPPORTED_AGENTS = [
36-
"alice@company.com",
37-
"bob@company.com",
38-
"carol@company.com",
39-
]
40-
41-
POLL_INTERVAL_SECONDS = 30
16+
POLL_INTERVAL = 30
4217
BUSY_ACTIVITIES = {"InACall", "InAConferenceCall", "InAMeeting", "Presenting"}
4318

4419

45-
# ---------------------------------------------------------------------------
46-
# Business logic helpers
47-
# ---------------------------------------------------------------------------
48-
49-
5020
def is_available(presence) -> bool:
51-
"""Determine if a user is considered 'available' for routing."""
5221
availability = presence.get_property("availability")
5322
activity = presence.get_property("activity")
5423
if not availability or not activity:
@@ -61,175 +30,73 @@ def is_available(presence) -> bool:
6130

6231

6332
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-
75-
def get_presence_for_user(client: GraphClient, user_id: str):
76-
"""Get presence for a single user by ID or UPN."""
77-
return client.communications.presences[user_id].get().execute_query()
78-
79-
80-
def get_presences_for_team(client: GraphClient, user_ids: List[str]):
81-
"""Get presence for multiple users in a single API call (bulk)."""
82-
return client.communications.get_presences_by_user_id(user_ids).execute_query()
33+
a = presence.get_property("availability") or "Unknown"
34+
b = presence.get_property("activity") or "Unknown"
35+
return f"availability={a}, activity={b}"
8336

8437

8538
def take_agent_offline(client: GraphClient, user_id: str):
86-
"""Mark an agent as DoNotDisturb + set an end-of-day status message."""
8739
expires = datetime.now(timezone.utc) + timedelta(hours=9)
88-
8940
client.communications.presences[user_id].set_user_preferred_presence(
90-
availability="DoNotDisturb",
91-
activity="OffWork",
92-
expiration_duration="PT9H",
41+
availability="DoNotDisturb", activity="OffWork", expiration_duration="PT9H",
9342
).execute_query()
94-
9543
client.communications.presences[user_id].set_status_message(
96-
message="Shift ended — back tomorrow",
97-
expiry=expires,
44+
message="Shift ended", expiry=expires,
9845
).execute_query()
9946

10047

101-
def set_agent_available(client: GraphClient, user_id: str):
102-
"""Return an agent to available state after break."""
48+
def start_shift(client: GraphClient, user_id: str):
49+
expires = datetime.now(timezone.utc) + timedelta(hours=9)
10350
client.communications.presences[user_id].set_user_preferred_presence(
104-
availability="Available",
105-
activity="Available",
106-
expiration_duration="PT8H",
51+
availability="Available", activity="Available", expiration_duration="PT9H",
52+
).execute_query()
53+
client.communications.presences[user_id].set_status_message(
54+
message="Online and ready", expiry=expires,
10755
).execute_query()
10856

10957

110-
# ---------------------------------------------------------------------------
111-
# Scenario 1: snapshot — check current team status
112-
# ---------------------------------------------------------------------------
113-
114-
115-
def snapshot_team_status(client: GraphClient, agents: List[str]):
116-
"""One-shot display of every agent's current presence."""
117-
print("=== Team presence snapshot ===")
118-
presences = get_presences_for_team(client, agents)
119-
for entry in presences:
120-
print(f" {entry.id:40s} {agent_status_summary(entry)}")
121-
print()
122-
123-
124-
# ---------------------------------------------------------------------------
125-
# Scenario 2: monitor — poll for state changes
126-
# ---------------------------------------------------------------------------
127-
128-
129-
def monitor_team_status(client: GraphClient, agents: List[str], cycles: int = 5):
130-
"""Poll presence for a team and report state changes."""
131-
print(f"=== Monitoring {len(agents)} agents (poll every {POLL_INTERVAL_SECONDS}s) ===")
58+
def find_available_agent(client: GraphClient, agents: list[str]) -> Optional[str]:
59+
presences = client.communications.get_presences_by_user_id(agents).execute_query()
60+
for e in presences:
61+
if is_available(e):
62+
return e.id
63+
return None
13264

133-
previous: Dict[str, str] = {}
13465

66+
def monitor_presence(client: GraphClient, agents: list[str], cycles: int = 3):
67+
print(f"Monitoring {len(agents)} agents (poll every {POLL_INTERVAL}s)")
68+
previous = {}
13569
for cycle in range(1, cycles + 1):
136-
print(f"\n--- Cycle {cycle}/{cycles} [{datetime.now(timezone.utc).isoformat()}] ---")
137-
presences = get_presences_for_team(client, agents)
138-
139-
for entry in presences:
140-
user_id = entry.id
141-
state = agent_status_summary(entry)
142-
old_state = previous.get(user_id)
143-
144-
if old_state is None:
145-
print(f" INIT {user_id:35s} {state}")
146-
elif state != old_state:
147-
print(f" >>> {user_id:35s} {old_state} -> {state}")
148-
if is_available(entry) and not is_available.__doc__:
149-
print(f" Agent {user_id} is now available for routing")
150-
else:
151-
print(f" . {user_id:35s} {state}")
152-
153-
previous[user_id] = state
154-
70+
presences = client.communications.get_presences_by_user_id(agents).execute_query()
71+
for e in presences:
72+
state = agent_status_summary(e)
73+
old = previous.get(e.id)
74+
if old is None:
75+
print(f" INIT {e.id}: {state}")
76+
elif state != old:
77+
print(f" CHG {e.id}: {old} -> {state}")
78+
previous[e.id] = state
15579
if cycle < cycles:
156-
sleep(POLL_INTERVAL_SECONDS)
157-
158-
print()
159-
160-
161-
# ---------------------------------------------------------------------------
162-
# Scenario 3: round-robin availability check for routing
163-
# ---------------------------------------------------------------------------
80+
sleep(POLL_INTERVAL)
16481

16582

166-
def find_available_agent(client: GraphClient, agents: List[str]) -> Optional[str]:
167-
"""Return the first available agent (useful for automatic call routing)."""
168-
presences = get_presences_for_team(client, agents)
169-
for entry in presences:
170-
if is_available(entry):
171-
return entry.id
172-
return None
173-
174-
175-
# ---------------------------------------------------------------------------
176-
# Scenario 4: set status message on shift start
177-
# ---------------------------------------------------------------------------
83+
def main():
84+
client = GraphClient().with_device_flow(client_id="your_client_id")
85+
agents = ["alice@company.com", "bob@company.com", "carol@company.com"]
17886

87+
presences = client.communications.get_presences_by_user_id(agents).execute_query()
88+
print("Team presence:")
89+
for e in presences:
90+
print(f" {e.id}: {agent_status_summary(e)}")
17991

180-
def start_shift(client: GraphClient, user_id: str, agent_name: str):
181-
"""Begin shift: set available presence with a status message."""
182-
expires = datetime.now(timezone.utc) + timedelta(hours=9)
183-
client.communications.presences[user_id].set_user_preferred_presence(
184-
availability="Available",
185-
activity="Available",
186-
expiration_duration="PT9H",
187-
).execute_query()
92+
available = find_available_agent(client, agents)
93+
print(f"\nRouting: {'call to ' + available if available else 'no agents — queue'}")
18894

189-
client.communications.presences[user_id].set_status_message(
190-
message=f"{agent_name} is online — ready to take calls",
191-
expiry=expires,
192-
).execute_query()
95+
monitor_presence(client, agents, cycles=3)
19396

97+
take_agent_offline(client, agents[0])
98+
print(f"\n{agents[0]} marked offline")
19499

195-
# ---------------------------------------------------------------------------
196-
# Main — run all scenarios
197-
# ---------------------------------------------------------------------------
198100

199101
if __name__ == "__main__":
200-
# Connect (choose one auth method)
201-
#
202-
# Option A: certificate
203-
# client = GraphClient(tenant).with_client_certificate(client_id, thumbprint, key)
204-
#
205-
# Option B: client secret
206-
# client = GraphClient(tenant).with_client_secret(client_id, client_secret)
207-
#
208-
# Option C: device code (interactive, works with MFA)
209-
client = GraphClient().with_device_flow(client_id="your_client_id")
210-
# (For brevity replace with real credentials above)
211-
212-
# ------------------------------------------------------------------
213-
# Scenario 1 — read-only snapshot
214-
# ------------------------------------------------------------------
215-
snapshot_team_status(client, SUPPORTED_AGENTS)
216-
217-
# ------------------------------------------------------------------
218-
# Scenario 2 — find someone to route a call to
219-
# ------------------------------------------------------------------
220-
available = find_available_agent(client, SUPPORTED_AGENTS)
221-
if available:
222-
print(f"Routing call to: {available}")
223-
else:
224-
print("No agents available — queuing call")
225-
226-
# ------------------------------------------------------------------
227-
# Scenario 3 — monitor for state changes (run 3 cycles)
228-
# ------------------------------------------------------------------
229-
monitor_team_status(client, SUPPORTED_AGENTS, cycles=3)
230-
231-
# ------------------------------------------------------------------
232-
# Scenario 4 — mark agent as off (end of shift)
233-
# ------------------------------------------------------------------
234-
take_agent_offline(client, SUPPORTED_AGENTS[0])
235-
print(f"Agent {SUPPORTED_AGENTS[0]} marked as offline")
102+
main()

0 commit comments

Comments
 (0)