teams-transcripts 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- legacy/__init__.py +0 -0
- legacy/transcript_download.py +3969 -0
- teams_transcripts/__init__.py +1 -0
- teams_transcripts/__main__.py +288 -0
- teams_transcripts/adapters/__init__.py +1 -0
- teams_transcripts/adapters/cdp/__init__.py +1 -0
- teams_transcripts/adapters/cdp/browser.py +196 -0
- teams_transcripts/adapters/cdp/connection.py +168 -0
- teams_transcripts/adapters/cdp/helpers.py +449 -0
- teams_transcripts/adapters/cdp/teams.py +463 -0
- teams_transcripts/adapters/vertex.py +274 -0
- teams_transcripts/domain/__init__.py +1 -0
- teams_transcripts/domain/chat.py +115 -0
- teams_transcripts/domain/html.py +126 -0
- teams_transcripts/domain/mcps.py +343 -0
- teams_transcripts/domain/models.py +531 -0
- teams_transcripts/domain/participants.py +292 -0
- teams_transcripts/domain/scoring.py +106 -0
- teams_transcripts/domain/speakers.py +130 -0
- teams_transcripts/domain/summary.py +266 -0
- teams_transcripts/domain/timestamps.py +350 -0
- teams_transcripts/domain/vtt.py +444 -0
- teams_transcripts/infrastructure/__init__.py +1 -0
- teams_transcripts/infrastructure/config.py +987 -0
- teams_transcripts/infrastructure/errors.py +120 -0
- teams_transcripts/infrastructure/logging.py +69 -0
- teams_transcripts/infrastructure/signals.py +143 -0
- teams_transcripts/mcp_server.py +818 -0
- teams_transcripts/ports/__init__.py +1 -0
- teams_transcripts/ports/agent.py +58 -0
- teams_transcripts/ports/browser.py +126 -0
- teams_transcripts/services/__init__.py +1 -0
- teams_transcripts/services/downloader.py +1283 -0
- teams_transcripts/services/extraction.py +1603 -0
- teams_transcripts/services/listing.py +826 -0
- teams_transcripts/services/navigation.py +1721 -0
- teams_transcripts/services/output.py +84 -0
- teams_transcripts/services/tenant.py +684 -0
- teams_transcripts-0.5.0.dist-info/METADATA +1438 -0
- teams_transcripts-0.5.0.dist-info/RECORD +42 -0
- teams_transcripts-0.5.0.dist-info/WHEEL +4 -0
- teams_transcripts-0.5.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""CDP HTTP helpers and target discovery.
|
|
2
|
+
|
|
3
|
+
Provides functions for interacting with CDP's HTTP endpoints and
|
|
4
|
+
discovering specific browser targets (Teams pages, SharePoint iframes).
|
|
5
|
+
|
|
6
|
+
Extracted from ``transcript_download.py`` lines 580-588 (cdp_http_get),
|
|
7
|
+
793-809 (find_teams_targets), 817-829 (find_search_target),
|
|
8
|
+
768-785 (verify_cdp), 1897-1908 (find_xplatplugins_targets),
|
|
9
|
+
1911-1993 (find_iframe_target).
|
|
10
|
+
|
|
11
|
+
Functions that were both I/O and filtering have been split:
|
|
12
|
+
* Pure filtering: ``filter_teams_targets``, ``filter_xplatplugins_targets``
|
|
13
|
+
* I/O orchestrators: ``find_teams_targets``, ``find_xplatplugins_targets``,
|
|
14
|
+
``find_search_target``, ``find_iframe_target``, ``verify_cdp``
|
|
15
|
+
|
|
16
|
+
Schema version: 1.0.0
|
|
17
|
+
Created: 2026-04-25
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import typing
|
|
24
|
+
import urllib.request
|
|
25
|
+
|
|
26
|
+
from teams_transcripts.adapters.cdp.connection import connect_to_target
|
|
27
|
+
from teams_transcripts.infrastructure.errors import (
|
|
28
|
+
EXIT_CDP_UNAVAILABLE,
|
|
29
|
+
EXIT_DOWNLOAD_FAILED,
|
|
30
|
+
exit_error,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if typing.TYPE_CHECKING:
|
|
34
|
+
from teams_transcripts.domain.models import MeetingConfig
|
|
35
|
+
from teams_transcripts.infrastructure.logging import Logger
|
|
36
|
+
from teams_transcripts.infrastructure.signals import ShutdownCoordinator
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# HTTP helpers
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cdp_http_get(port: int, path: str) -> dict | list | None:
|
|
45
|
+
"""GET a CDP HTTP endpoint, return parsed JSON or ``None`` on failure.
|
|
46
|
+
|
|
47
|
+
Only connects to 127.0.0.1 -- the URL is constructed internally and
|
|
48
|
+
never from external input.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
port: CDP debugging port number.
|
|
52
|
+
path: URL path (e.g. ``"/json/version"``).
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Parsed JSON (dict or list), or ``None`` if the request failed.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: If *port* is not in the valid range or *path* does
|
|
59
|
+
not start with ``/``.
|
|
60
|
+
"""
|
|
61
|
+
if not (1 <= port <= 65535):
|
|
62
|
+
msg = f"Port out of range: {port}"
|
|
63
|
+
raise ValueError(msg)
|
|
64
|
+
if not path.startswith("/"):
|
|
65
|
+
msg = f"Path must start with '/': {path!r}"
|
|
66
|
+
raise ValueError(msg)
|
|
67
|
+
url = f"http://127.0.0.1:{port}{path}"
|
|
68
|
+
try:
|
|
69
|
+
with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310 -- localhost only
|
|
70
|
+
return json.loads(resp.read())
|
|
71
|
+
except Exception:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Target filtering (pure)
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def filter_teams_targets(targets: list[dict]) -> list[dict]:
|
|
81
|
+
"""Filter a target list to main Teams v2 page targets.
|
|
82
|
+
|
|
83
|
+
Excludes service workers and other non-page targets.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
targets: Raw CDP target list from ``/json``.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Filtered list of Teams page targets.
|
|
90
|
+
"""
|
|
91
|
+
results = []
|
|
92
|
+
for t in targets:
|
|
93
|
+
url = t.get("url", "")
|
|
94
|
+
is_teams_v2 = "teams.microsoft.com/v2" in url
|
|
95
|
+
is_teams_live = "teams.live.com/v2" in url
|
|
96
|
+
is_m365_chat = "microsoft365.com/chat" in url
|
|
97
|
+
is_page = "worker" not in url
|
|
98
|
+
if (is_teams_v2 or is_teams_live or is_m365_chat) and is_page:
|
|
99
|
+
results.append(t)
|
|
100
|
+
return results
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def filter_xplatplugins_targets(targets: list[dict]) -> list[dict]:
|
|
104
|
+
"""Filter a target list to SharePoint transcript iframe targets.
|
|
105
|
+
|
|
106
|
+
Matches targets whose URL contains both ``sharepoint`` and
|
|
107
|
+
``xplatplugins``. This is the only pattern observed in production
|
|
108
|
+
for the Teams transcript viewer iframe (hosted at
|
|
109
|
+
``/_layouts/15/xplatplugins.aspx``).
|
|
110
|
+
|
|
111
|
+
The filter is deliberately narrow to avoid false positives from
|
|
112
|
+
other SharePoint iframes (SPFx portals, OneDrive file browser,
|
|
113
|
+
Stream embeds, etc.) which would cause iframe disambiguation
|
|
114
|
+
failures.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
targets: Raw CDP target list from ``/json``.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Filtered list of SharePoint transcript iframe targets.
|
|
121
|
+
"""
|
|
122
|
+
matches = []
|
|
123
|
+
for t in targets:
|
|
124
|
+
url = t.get("url", "")
|
|
125
|
+
if "sharepoint" in url and "xplatplugins" in url:
|
|
126
|
+
matches.append(t)
|
|
127
|
+
return matches
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def filter_worker_targets(targets: list[dict], tenant_id: str | None = None) -> list[dict]:
|
|
131
|
+
"""Filter a target list to Teams precompiled web worker targets.
|
|
132
|
+
|
|
133
|
+
Workers are the per-tenant background threads that host the GraphQL
|
|
134
|
+
handlers, MCPS workflow starters, and token acquisition services.
|
|
135
|
+
They have ``type: "worker"`` in the CDP target list and a URL
|
|
136
|
+
containing ``precompiled-web-worker``.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
targets: Raw CDP target list from ``/json``.
|
|
140
|
+
tenant_id: Optional tenant ID to prefer. When provided,
|
|
141
|
+
workers whose URL contains this tenant ID are returned
|
|
142
|
+
first, followed by other matching workers.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Filtered list of worker target dicts, ordered by tenant
|
|
146
|
+
match preference (tenant-matching first).
|
|
147
|
+
"""
|
|
148
|
+
tenant_matches = []
|
|
149
|
+
other_matches = []
|
|
150
|
+
for t in targets:
|
|
151
|
+
url = t.get("url", "")
|
|
152
|
+
target_type = t.get("type", "")
|
|
153
|
+
is_worker = "precompiled-web-worker" in url or target_type == "other"
|
|
154
|
+
if not is_worker:
|
|
155
|
+
continue
|
|
156
|
+
if tenant_id and tenant_id in url:
|
|
157
|
+
tenant_matches.append(t)
|
|
158
|
+
else:
|
|
159
|
+
other_matches.append(t)
|
|
160
|
+
return tenant_matches + other_matches
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# I/O orchestrators
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def find_teams_targets(
|
|
169
|
+
port: int,
|
|
170
|
+
*,
|
|
171
|
+
logger: Logger,
|
|
172
|
+
format_json: bool,
|
|
173
|
+
) -> list[dict]:
|
|
174
|
+
"""Discover main Teams page targets via CDP HTTP.
|
|
175
|
+
|
|
176
|
+
Calls ``cdp_http_get`` and filters the result. Exits with
|
|
177
|
+
``CDP_UNAVAILABLE`` if no targets can be listed.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
port: CDP debugging port.
|
|
181
|
+
logger: Logger instance.
|
|
182
|
+
format_json: Whether to format errors as JSON.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of Teams page target dicts.
|
|
186
|
+
"""
|
|
187
|
+
raw_targets = cdp_http_get(port, "/json")
|
|
188
|
+
if not raw_targets or not isinstance(raw_targets, list):
|
|
189
|
+
exit_error(
|
|
190
|
+
EXIT_CDP_UNAVAILABLE,
|
|
191
|
+
"CDP_UNAVAILABLE",
|
|
192
|
+
"Could not list CDP targets.",
|
|
193
|
+
step="step3",
|
|
194
|
+
format_json=format_json,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return filter_teams_targets(raw_targets)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def find_worker_targets(
|
|
201
|
+
port: int,
|
|
202
|
+
*,
|
|
203
|
+
logger: Logger,
|
|
204
|
+
tenant_id: str | None = None,
|
|
205
|
+
) -> list[dict]:
|
|
206
|
+
"""Discover Teams web worker targets via CDP HTTP.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
port: CDP debugging port.
|
|
210
|
+
logger: Logger instance.
|
|
211
|
+
tenant_id: Optional tenant ID to filter workers by.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of worker target dicts (may be empty).
|
|
215
|
+
"""
|
|
216
|
+
raw_targets = cdp_http_get(port, "/json")
|
|
217
|
+
if not raw_targets or not isinstance(raw_targets, list):
|
|
218
|
+
return []
|
|
219
|
+
workers = filter_worker_targets(raw_targets, tenant_id=tenant_id)
|
|
220
|
+
logger.log("list-transcripts", f"Found {len(workers)} worker target(s)")
|
|
221
|
+
return workers
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def find_xplatplugins_targets_io(port: int) -> list[dict]:
|
|
225
|
+
"""Discover xplatplugins iframe targets via CDP HTTP.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
port: CDP debugging port.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
List of xplatplugins target dicts (may be empty).
|
|
232
|
+
"""
|
|
233
|
+
raw_targets = cdp_http_get(port, "/json")
|
|
234
|
+
if not raw_targets or not isinstance(raw_targets, list):
|
|
235
|
+
return []
|
|
236
|
+
return filter_xplatplugins_targets(raw_targets)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _search_target_sort_key(target: dict) -> tuple[int, str]:
|
|
240
|
+
"""Sort key that prioritises the primary Teams page over tenant pages.
|
|
241
|
+
|
|
242
|
+
The primary Teams page (titled ``"Microsoft Teams"``) is the most
|
|
243
|
+
stable target for keyboard-driven navigation. Tenant-specific
|
|
244
|
+
pages (e.g. ``"Central Hub | insidemedia.net"``) can navigate away
|
|
245
|
+
when ``Ctrl+3`` is sent, destroying the WebSocket connection.
|
|
246
|
+
|
|
247
|
+
Returns a ``(priority, title)`` tuple so targets with the same
|
|
248
|
+
priority are ordered alphabetically by title for determinism.
|
|
249
|
+
"""
|
|
250
|
+
title = target.get("title", "")
|
|
251
|
+
if title == "Microsoft Teams":
|
|
252
|
+
return (0, title)
|
|
253
|
+
return (1, title)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def find_search_target(
|
|
257
|
+
port: int,
|
|
258
|
+
targets: list[dict],
|
|
259
|
+
*,
|
|
260
|
+
logger: Logger,
|
|
261
|
+
) -> str | None:
|
|
262
|
+
"""Find which CDP target has the Teams search input element.
|
|
263
|
+
|
|
264
|
+
Targets are checked in priority order: pages titled
|
|
265
|
+
``"Microsoft Teams"`` are tried first because they are the most
|
|
266
|
+
stable target for subsequent keyboard navigation.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
port: CDP debugging port.
|
|
270
|
+
targets: List of candidate target dicts.
|
|
271
|
+
logger: Logger instance.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
The target ID with the search input, or ``None``.
|
|
275
|
+
"""
|
|
276
|
+
sorted_targets = sorted(targets, key=_search_target_sort_key)
|
|
277
|
+
for t in sorted_targets:
|
|
278
|
+
target_id = await _check_target_has_search(port, t, logger)
|
|
279
|
+
if target_id is not None:
|
|
280
|
+
logger.log("step4", f"Search input found on target: {target_id[:20]}...")
|
|
281
|
+
return target_id
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
async def _check_target_has_search(port: int, target: dict, logger: Logger) -> str | None:
|
|
286
|
+
"""Check whether a single CDP target contains the search input.
|
|
287
|
+
|
|
288
|
+
Returns the target ID if found, ``None`` otherwise. Connection
|
|
289
|
+
failures and timeouts are logged and treated as "not found".
|
|
290
|
+
"""
|
|
291
|
+
try:
|
|
292
|
+
ws, cdp = await connect_to_target(port, target["id"])
|
|
293
|
+
async with ws:
|
|
294
|
+
result = await cdp.evaluate("!!document.querySelector('#ms-searchux-input')")
|
|
295
|
+
if result is True:
|
|
296
|
+
return target["id"]
|
|
297
|
+
except Exception as exc:
|
|
298
|
+
logger.log("step4", f"Target {target['id'][:20]}... check failed: {exc}")
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def verify_cdp(
|
|
303
|
+
port: int,
|
|
304
|
+
*,
|
|
305
|
+
logger: Logger,
|
|
306
|
+
shutdown: ShutdownCoordinator,
|
|
307
|
+
format_json: bool,
|
|
308
|
+
) -> str:
|
|
309
|
+
"""Verify CDP is responding and return the browser version string.
|
|
310
|
+
|
|
311
|
+
Retries up to 5 times with 3-second intervals. Exits with
|
|
312
|
+
``CDP_UNAVAILABLE`` if all attempts fail.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
port: CDP debugging port.
|
|
316
|
+
logger: Logger instance.
|
|
317
|
+
shutdown: Shutdown coordinator for interruptibility.
|
|
318
|
+
format_json: Whether to format errors as JSON.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Browser version string (e.g. ``"Chrome/123.0.6312.86"``).
|
|
322
|
+
"""
|
|
323
|
+
for attempt in range(5):
|
|
324
|
+
shutdown.check_shutdown()
|
|
325
|
+
version = cdp_http_get(port, "/json/version")
|
|
326
|
+
if isinstance(version, dict) and "Browser" in version:
|
|
327
|
+
browser = version["Browser"]
|
|
328
|
+
logger.log("step2", f"CDP verified: {browser}")
|
|
329
|
+
return browser
|
|
330
|
+
logger.log("step2", f"Attempt {attempt + 1}/5 -- waiting...")
|
|
331
|
+
shutdown.interruptible_sleep_sync(3)
|
|
332
|
+
|
|
333
|
+
# exit_error calls sys.exit (typed NoReturn) -- unreachable beyond this point
|
|
334
|
+
exit_error(
|
|
335
|
+
EXIT_CDP_UNAVAILABLE,
|
|
336
|
+
"CDP_UNAVAILABLE",
|
|
337
|
+
"CDP did not respond after 5 attempts. Check Teams is running.",
|
|
338
|
+
step="step2",
|
|
339
|
+
format_json=format_json,
|
|
340
|
+
)
|
|
341
|
+
raise SystemExit(EXIT_CDP_UNAVAILABLE)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def find_iframe_target(
|
|
345
|
+
port: int,
|
|
346
|
+
*,
|
|
347
|
+
config: MeetingConfig,
|
|
348
|
+
agent_client: typing.Any,
|
|
349
|
+
logger: Logger,
|
|
350
|
+
shutdown: ShutdownCoordinator,
|
|
351
|
+
pre_nav_iframe_ids: set[str] | None = None,
|
|
352
|
+
allow_missing: bool = False,
|
|
353
|
+
) -> str | None:
|
|
354
|
+
"""Find the correct xplatplugins iframe target.
|
|
355
|
+
|
|
356
|
+
Uses the LLM agent if multiple candidates are found and
|
|
357
|
+
disambiguation is needed. Falls back to the most recent target.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
port: CDP debugging port.
|
|
361
|
+
config: Meeting configuration.
|
|
362
|
+
agent_client: Vertex AI agent client.
|
|
363
|
+
logger: Logger instance.
|
|
364
|
+
shutdown: Shutdown coordinator.
|
|
365
|
+
pre_nav_iframe_ids: Set of iframe target IDs that existed
|
|
366
|
+
before navigation (to prefer newly created targets).
|
|
367
|
+
allow_missing: When ``True``, return ``None`` instead of
|
|
368
|
+
calling :func:`exit_error` when no targets are found.
|
|
369
|
+
Used by the multi-part download loop to retry navigation
|
|
370
|
+
rather than hard-exiting.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
The selected target ID, or ``None`` when *allow_missing* is
|
|
374
|
+
set and no targets were found.
|
|
375
|
+
"""
|
|
376
|
+
from teams_transcripts.adapters.vertex import agent_text_query
|
|
377
|
+
|
|
378
|
+
logger.log("step6", "Looking for transcript iframe target...")
|
|
379
|
+
pre_nav_iframe_ids = pre_nav_iframe_ids or set()
|
|
380
|
+
|
|
381
|
+
# Retry as the iframe may still be loading
|
|
382
|
+
matches: list[dict] = []
|
|
383
|
+
new_matches: list[dict] = []
|
|
384
|
+
for attempt in range(10):
|
|
385
|
+
shutdown.check_shutdown()
|
|
386
|
+
matches = find_xplatplugins_targets_io(port)
|
|
387
|
+
new_matches = [t for t in matches if t["id"] not in pre_nav_iframe_ids]
|
|
388
|
+
if new_matches:
|
|
389
|
+
break
|
|
390
|
+
if matches and attempt >= 5:
|
|
391
|
+
logger.log("step6", "No new iframe targets appeared. Using existing targets.")
|
|
392
|
+
new_matches = matches
|
|
393
|
+
break
|
|
394
|
+
label = "new iframe" if matches else "any iframe"
|
|
395
|
+
logger.log("step6", f"Waiting for {label} target (attempt {attempt + 1}/10)...")
|
|
396
|
+
shutdown.interruptible_sleep_sync(2)
|
|
397
|
+
|
|
398
|
+
candidates = new_matches or matches
|
|
399
|
+
|
|
400
|
+
if not candidates:
|
|
401
|
+
if allow_missing:
|
|
402
|
+
logger.log("step6", "No iframe targets found (will retry).")
|
|
403
|
+
return None
|
|
404
|
+
exit_error(
|
|
405
|
+
EXIT_DOWNLOAD_FAILED,
|
|
406
|
+
"IFRAME_NOT_FOUND",
|
|
407
|
+
"No xplatplugins iframe target found. "
|
|
408
|
+
"Check that Step 5 completed and the Transcript tab loaded.",
|
|
409
|
+
step="step6",
|
|
410
|
+
format_json=config.format_json,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if len(candidates) == 1:
|
|
414
|
+
tid = candidates[0]["id"]
|
|
415
|
+
is_new = tid not in pre_nav_iframe_ids
|
|
416
|
+
logger.log("step6", f"{'New' if is_new else 'Existing'} iframe target: {tid[:20]}...")
|
|
417
|
+
return tid
|
|
418
|
+
|
|
419
|
+
# Multiple targets -- ask the agent
|
|
420
|
+
logger.log("step6", f"{len(candidates)} iframe targets to choose from. Asking agent...")
|
|
421
|
+
|
|
422
|
+
target_list = "\n".join(
|
|
423
|
+
f" [{i}] ID: {t['id']}, URL: {t['url'][:200]}" for i, t in enumerate(candidates)
|
|
424
|
+
)
|
|
425
|
+
prompt = (
|
|
426
|
+
f"Multiple SharePoint iframe targets were found in a Teams CDP session. "
|
|
427
|
+
f"I am looking for the transcript of the meeting "
|
|
428
|
+
f"'{config.meeting}' on {config.date}.\n\n"
|
|
429
|
+
f"Targets:\n{target_list}\n\n"
|
|
430
|
+
f"Which target most likely corresponds to this meeting? "
|
|
431
|
+
f"Look at the URL paths for clues (e.g., organiser name, meeting context). "
|
|
432
|
+
f'Return JSON: {{"target_id": "<full target ID>"}} '
|
|
433
|
+
f"for the best match."
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
result = agent_text_query(agent_client, config.model, prompt, logger=logger)
|
|
437
|
+
|
|
438
|
+
if result and result.get("target_id"):
|
|
439
|
+
for t in candidates:
|
|
440
|
+
if t["id"] == result["target_id"]:
|
|
441
|
+
logger.log("step6", f"Agent selected target: {t['id'][:20]}...")
|
|
442
|
+
return t["id"]
|
|
443
|
+
for t in candidates:
|
|
444
|
+
if result["target_id"] in t["id"] or t["id"] in result["target_id"]:
|
|
445
|
+
logger.log("step6", f"Agent selected target (partial match): {t['id'][:20]}...")
|
|
446
|
+
return t["id"]
|
|
447
|
+
|
|
448
|
+
logger.log("step6", "Agent could not disambiguate. Using most recent target.")
|
|
449
|
+
return candidates[-1]["id"]
|