devscontext 0.1.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.
- devscontext/__init__.py +3 -0
- devscontext/adapters/__init__.py +23 -0
- devscontext/adapters/base.py +105 -0
- devscontext/adapters/fireflies.py +585 -0
- devscontext/adapters/gmail.py +580 -0
- devscontext/adapters/jira.py +639 -0
- devscontext/adapters/local_docs.py +984 -0
- devscontext/adapters/slack.py +804 -0
- devscontext/agents/__init__.py +28 -0
- devscontext/agents/preprocessor.py +775 -0
- devscontext/agents/watcher.py +265 -0
- devscontext/cache.py +151 -0
- devscontext/cli.py +727 -0
- devscontext/config.py +264 -0
- devscontext/constants.py +107 -0
- devscontext/core.py +582 -0
- devscontext/exceptions.py +148 -0
- devscontext/logging.py +181 -0
- devscontext/models.py +504 -0
- devscontext/plugins/__init__.py +49 -0
- devscontext/plugins/base.py +321 -0
- devscontext/plugins/registry.py +544 -0
- devscontext/py.typed +0 -0
- devscontext/rag/__init__.py +113 -0
- devscontext/rag/embeddings.py +296 -0
- devscontext/rag/index.py +323 -0
- devscontext/server.py +374 -0
- devscontext/storage.py +321 -0
- devscontext/synthesis.py +1057 -0
- devscontext/utils.py +297 -0
- devscontext-0.1.0.dist-info/METADATA +253 -0
- devscontext-0.1.0.dist-info/RECORD +35 -0
- devscontext-0.1.0.dist-info/WHEEL +4 -0
- devscontext-0.1.0.dist-info/entry_points.txt +2 -0
- devscontext-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"""Fireflies adapter for fetching meeting transcript context.
|
|
2
|
+
|
|
3
|
+
This adapter connects to the Fireflies.ai GraphQL API to fetch meeting
|
|
4
|
+
transcripts and search for relevant discussions related to a task.
|
|
5
|
+
|
|
6
|
+
This adapter implements the Adapter interface for the plugin system.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
config = FirefliesConfig(api_key="your-api-key", enabled=True)
|
|
10
|
+
adapter = FirefliesAdapter(config)
|
|
11
|
+
context = await adapter.fetch_task_context("PROJ-123")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
from devscontext.constants import (
|
|
23
|
+
ADAPTER_FIREFLIES,
|
|
24
|
+
DEFAULT_HTTP_TIMEOUT_SECONDS,
|
|
25
|
+
FIREFLIES_API_URL,
|
|
26
|
+
FIREFLIES_CONTEXT_WINDOW,
|
|
27
|
+
FIREFLIES_SEARCH_LIMIT,
|
|
28
|
+
SOURCE_TYPE_MEETING,
|
|
29
|
+
)
|
|
30
|
+
from devscontext.logging import get_logger
|
|
31
|
+
from devscontext.models import ContextData, FirefliesConfig, MeetingContext, MeetingExcerpt
|
|
32
|
+
from devscontext.plugins.base import Adapter, SearchResult, SourceContext
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from devscontext.models import JiraTicket
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
# GraphQL query to search transcripts
|
|
40
|
+
SEARCH_TRANSCRIPTS_QUERY = """
|
|
41
|
+
query SearchTranscripts($query: String!, $limit: Int!) {
|
|
42
|
+
transcripts(filter_string: $query, limit: $limit) {
|
|
43
|
+
id
|
|
44
|
+
title
|
|
45
|
+
date
|
|
46
|
+
participants
|
|
47
|
+
summary {
|
|
48
|
+
overview
|
|
49
|
+
action_items
|
|
50
|
+
keywords
|
|
51
|
+
}
|
|
52
|
+
sentences {
|
|
53
|
+
text
|
|
54
|
+
speaker_name
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FirefliesAdapter(Adapter):
|
|
62
|
+
"""Adapter for fetching context from Fireflies meeting transcripts.
|
|
63
|
+
|
|
64
|
+
Implements the Adapter interface for the plugin system.
|
|
65
|
+
Connects to Fireflies.ai to search for meeting transcripts
|
|
66
|
+
that mention a specific task ID or keywords.
|
|
67
|
+
|
|
68
|
+
Class Attributes:
|
|
69
|
+
name: Adapter identifier ("fireflies").
|
|
70
|
+
source_type: Source category ("meeting").
|
|
71
|
+
config_schema: Configuration model (FirefliesConfig).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Adapter class attributes
|
|
75
|
+
name: ClassVar[str] = ADAPTER_FIREFLIES
|
|
76
|
+
source_type: ClassVar[str] = SOURCE_TYPE_MEETING
|
|
77
|
+
config_schema: ClassVar[type[FirefliesConfig]] = FirefliesConfig
|
|
78
|
+
|
|
79
|
+
def __init__(self, config: FirefliesConfig) -> None:
|
|
80
|
+
"""Initialize the Fireflies adapter.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config: Fireflies configuration with API key.
|
|
84
|
+
"""
|
|
85
|
+
self._config = config
|
|
86
|
+
self._client: httpx.AsyncClient | None = None
|
|
87
|
+
|
|
88
|
+
def _get_client(self) -> httpx.AsyncClient:
|
|
89
|
+
"""Get or create the HTTP client."""
|
|
90
|
+
if self._client is None:
|
|
91
|
+
self._client = httpx.AsyncClient(
|
|
92
|
+
headers={
|
|
93
|
+
"Authorization": f"Bearer {self._config.api_key}",
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
},
|
|
96
|
+
timeout=DEFAULT_HTTP_TIMEOUT_SECONDS,
|
|
97
|
+
)
|
|
98
|
+
return self._client
|
|
99
|
+
|
|
100
|
+
async def close(self) -> None:
|
|
101
|
+
"""Close the HTTP client."""
|
|
102
|
+
if self._client is not None:
|
|
103
|
+
await self._client.aclose()
|
|
104
|
+
self._client = None
|
|
105
|
+
|
|
106
|
+
async def search_transcripts(self, query: str) -> list[dict[str, Any]]:
|
|
107
|
+
"""Search Fireflies transcripts by query string.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
query: Search query (e.g., ticket ID or keywords).
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of transcript data dictionaries.
|
|
114
|
+
"""
|
|
115
|
+
client = self._get_client()
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
response = await client.post(
|
|
119
|
+
FIREFLIES_API_URL,
|
|
120
|
+
json={
|
|
121
|
+
"query": SEARCH_TRANSCRIPTS_QUERY,
|
|
122
|
+
"variables": {"query": query, "limit": FIREFLIES_SEARCH_LIMIT},
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
response.raise_for_status()
|
|
126
|
+
data = response.json()
|
|
127
|
+
|
|
128
|
+
if "errors" in data:
|
|
129
|
+
logger.warning(
|
|
130
|
+
"Fireflies GraphQL errors",
|
|
131
|
+
extra={"errors": data["errors"], "query": query},
|
|
132
|
+
)
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
transcripts = data.get("data", {}).get("transcripts") or []
|
|
136
|
+
logger.info(
|
|
137
|
+
"Fireflies search completed",
|
|
138
|
+
extra={"query": query, "count": len(transcripts)},
|
|
139
|
+
)
|
|
140
|
+
return transcripts
|
|
141
|
+
|
|
142
|
+
except httpx.HTTPStatusError as e:
|
|
143
|
+
logger.warning(
|
|
144
|
+
"Fireflies API error",
|
|
145
|
+
extra={"status_code": e.response.status_code, "query": query},
|
|
146
|
+
)
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
except httpx.RequestError as e:
|
|
150
|
+
logger.warning(
|
|
151
|
+
"Fireflies network error",
|
|
152
|
+
extra={"error": str(e), "query": query},
|
|
153
|
+
)
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
def _extract_relevant_excerpts(
|
|
157
|
+
self,
|
|
158
|
+
sentences: list[dict[str, Any]],
|
|
159
|
+
search_terms: list[str],
|
|
160
|
+
) -> str:
|
|
161
|
+
"""Extract relevant sentences from transcript with surrounding context.
|
|
162
|
+
|
|
163
|
+
Finds sentences mentioning search terms and includes ±N surrounding
|
|
164
|
+
sentences for context.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
sentences: List of sentence dictionaries with text and speaker_name.
|
|
168
|
+
search_terms: Terms to search for in sentences.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Formatted excerpt string with speaker names.
|
|
172
|
+
"""
|
|
173
|
+
if not sentences or not search_terms:
|
|
174
|
+
return ""
|
|
175
|
+
|
|
176
|
+
# Find indices of sentences containing search terms
|
|
177
|
+
matching_indices: set[int] = set()
|
|
178
|
+
search_pattern = re.compile(
|
|
179
|
+
"|".join(re.escape(term) for term in search_terms),
|
|
180
|
+
re.IGNORECASE,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
for i, sentence in enumerate(sentences):
|
|
184
|
+
text = sentence.get("text", "")
|
|
185
|
+
if search_pattern.search(text):
|
|
186
|
+
matching_indices.add(i)
|
|
187
|
+
|
|
188
|
+
if not matching_indices:
|
|
189
|
+
return ""
|
|
190
|
+
|
|
191
|
+
# Expand to include surrounding context
|
|
192
|
+
indices_to_include: set[int] = set()
|
|
193
|
+
for idx in matching_indices:
|
|
194
|
+
for offset in range(-FIREFLIES_CONTEXT_WINDOW, FIREFLIES_CONTEXT_WINDOW + 1):
|
|
195
|
+
new_idx = idx + offset
|
|
196
|
+
if 0 <= new_idx < len(sentences):
|
|
197
|
+
indices_to_include.add(new_idx)
|
|
198
|
+
|
|
199
|
+
# Build excerpt from consecutive ranges
|
|
200
|
+
sorted_indices = sorted(indices_to_include)
|
|
201
|
+
excerpt_parts: list[str] = []
|
|
202
|
+
current_range: list[int] = []
|
|
203
|
+
|
|
204
|
+
for idx in sorted_indices:
|
|
205
|
+
if not current_range or idx == current_range[-1] + 1:
|
|
206
|
+
current_range.append(idx)
|
|
207
|
+
else:
|
|
208
|
+
# Output current range and start new one
|
|
209
|
+
if current_range:
|
|
210
|
+
excerpt_parts.append(self._format_sentence_range(sentences, current_range))
|
|
211
|
+
current_range = [idx]
|
|
212
|
+
|
|
213
|
+
# Don't forget the last range
|
|
214
|
+
if current_range:
|
|
215
|
+
excerpt_parts.append(self._format_sentence_range(sentences, current_range))
|
|
216
|
+
|
|
217
|
+
return "\n\n[...]\n\n".join(excerpt_parts)
|
|
218
|
+
|
|
219
|
+
def _format_sentence_range(
|
|
220
|
+
self,
|
|
221
|
+
sentences: list[dict[str, Any]],
|
|
222
|
+
indices: list[int],
|
|
223
|
+
) -> str:
|
|
224
|
+
"""Format a range of sentences with speaker names.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
sentences: All sentences from the transcript.
|
|
228
|
+
indices: Indices of sentences to include.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Formatted string with speaker attributions.
|
|
232
|
+
"""
|
|
233
|
+
parts: list[str] = []
|
|
234
|
+
current_speaker: str | None = None
|
|
235
|
+
|
|
236
|
+
for idx in indices:
|
|
237
|
+
sentence = sentences[idx]
|
|
238
|
+
speaker = sentence.get("speaker_name", "Unknown")
|
|
239
|
+
text = sentence.get("text", "").strip()
|
|
240
|
+
|
|
241
|
+
if not text:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
if speaker != current_speaker:
|
|
245
|
+
if parts:
|
|
246
|
+
parts.append("") # Add blank line between speakers
|
|
247
|
+
parts.append(f"**{speaker}:** {text}")
|
|
248
|
+
current_speaker = speaker
|
|
249
|
+
else:
|
|
250
|
+
parts.append(text)
|
|
251
|
+
|
|
252
|
+
return "\n".join(parts)
|
|
253
|
+
|
|
254
|
+
def _parse_transcript(
|
|
255
|
+
self,
|
|
256
|
+
transcript: dict[str, Any],
|
|
257
|
+
search_terms: list[str],
|
|
258
|
+
) -> MeetingExcerpt | None:
|
|
259
|
+
"""Parse a transcript into a MeetingExcerpt.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
transcript: Raw transcript data from Fireflies API.
|
|
263
|
+
search_terms: Terms used for searching (for excerpt extraction).
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
MeetingExcerpt if valid, None otherwise.
|
|
267
|
+
"""
|
|
268
|
+
try:
|
|
269
|
+
# Parse date
|
|
270
|
+
date_str = transcript.get("date")
|
|
271
|
+
if date_str:
|
|
272
|
+
try:
|
|
273
|
+
meeting_date = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
|
274
|
+
if meeting_date.tzinfo is None:
|
|
275
|
+
meeting_date = meeting_date.replace(tzinfo=UTC)
|
|
276
|
+
except ValueError:
|
|
277
|
+
meeting_date = datetime.now(UTC)
|
|
278
|
+
else:
|
|
279
|
+
meeting_date = datetime.now(UTC)
|
|
280
|
+
|
|
281
|
+
# Get participants
|
|
282
|
+
participants = transcript.get("participants") or []
|
|
283
|
+
if isinstance(participants, str):
|
|
284
|
+
participants = [p.strip() for p in participants.split(",")]
|
|
285
|
+
|
|
286
|
+
# Get summary data
|
|
287
|
+
summary = transcript.get("summary") or {}
|
|
288
|
+
overview = summary.get("overview") or ""
|
|
289
|
+
action_items_raw = summary.get("action_items") or []
|
|
290
|
+
if isinstance(action_items_raw, str):
|
|
291
|
+
action_items = [
|
|
292
|
+
item.strip() for item in action_items_raw.split("\n") if item.strip()
|
|
293
|
+
]
|
|
294
|
+
else:
|
|
295
|
+
action_items = list(action_items_raw) if action_items_raw else []
|
|
296
|
+
|
|
297
|
+
# Extract relevant excerpts from sentences
|
|
298
|
+
sentences = transcript.get("sentences") or []
|
|
299
|
+
excerpt = self._extract_relevant_excerpts(sentences, search_terms)
|
|
300
|
+
|
|
301
|
+
# If no relevant excerpt found but we have an overview, use that
|
|
302
|
+
if not excerpt and overview:
|
|
303
|
+
excerpt = f"**Summary:** {overview}"
|
|
304
|
+
elif not excerpt:
|
|
305
|
+
return None # No useful content
|
|
306
|
+
|
|
307
|
+
return MeetingExcerpt(
|
|
308
|
+
meeting_title=transcript.get("title") or "Untitled Meeting",
|
|
309
|
+
meeting_date=meeting_date,
|
|
310
|
+
participants=participants,
|
|
311
|
+
excerpt=excerpt,
|
|
312
|
+
action_items=action_items,
|
|
313
|
+
decisions=[], # Fireflies doesn't provide decisions directly
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.warning(
|
|
318
|
+
"Failed to parse Fireflies transcript",
|
|
319
|
+
extra={"error": str(e), "transcript_id": transcript.get("id")},
|
|
320
|
+
)
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
async def get_meeting_context(self, task_id: str) -> MeetingContext:
|
|
324
|
+
"""Get meeting context for a task ID.
|
|
325
|
+
|
|
326
|
+
Searches for transcripts mentioning the task ID and extracts
|
|
327
|
+
relevant excerpts.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
task_id: The task identifier to search for.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
MeetingContext with relevant meeting excerpts.
|
|
334
|
+
"""
|
|
335
|
+
if not self._config.enabled:
|
|
336
|
+
logger.debug("Fireflies adapter is disabled")
|
|
337
|
+
return MeetingContext()
|
|
338
|
+
|
|
339
|
+
if not self._config.api_key:
|
|
340
|
+
logger.warning("Fireflies adapter missing API key")
|
|
341
|
+
return MeetingContext()
|
|
342
|
+
|
|
343
|
+
# Search by task ID
|
|
344
|
+
search_terms = [task_id]
|
|
345
|
+
transcripts = await self.search_transcripts(task_id)
|
|
346
|
+
|
|
347
|
+
# Parse transcripts into excerpts
|
|
348
|
+
excerpts: list[MeetingExcerpt] = []
|
|
349
|
+
for transcript in transcripts:
|
|
350
|
+
excerpt = self._parse_transcript(transcript, search_terms)
|
|
351
|
+
if excerpt:
|
|
352
|
+
excerpts.append(excerpt)
|
|
353
|
+
|
|
354
|
+
logger.info(
|
|
355
|
+
"Fireflies context assembled",
|
|
356
|
+
extra={"task_id": task_id, "meeting_count": len(excerpts)},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return MeetingContext(meetings=excerpts)
|
|
360
|
+
|
|
361
|
+
async def fetch_task_context(
|
|
362
|
+
self,
|
|
363
|
+
task_id: str,
|
|
364
|
+
ticket: JiraTicket | None = None,
|
|
365
|
+
) -> SourceContext:
|
|
366
|
+
"""Fetch context from Fireflies meeting transcripts.
|
|
367
|
+
|
|
368
|
+
Implements the Adapter interface. Searches for meetings mentioning
|
|
369
|
+
the task ID or keywords from the ticket.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
task_id: The task identifier to search for in transcripts.
|
|
373
|
+
ticket: Optional Jira ticket for keyword extraction.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
SourceContext with MeetingContext data.
|
|
377
|
+
"""
|
|
378
|
+
if not self._config.enabled:
|
|
379
|
+
logger.debug("Fireflies adapter is disabled")
|
|
380
|
+
return SourceContext(
|
|
381
|
+
source_name=self.name,
|
|
382
|
+
source_type=self.source_type,
|
|
383
|
+
data=None,
|
|
384
|
+
raw_text="",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
meeting_context = await self.get_meeting_context(task_id)
|
|
388
|
+
|
|
389
|
+
if not meeting_context.meetings:
|
|
390
|
+
return SourceContext(
|
|
391
|
+
source_name=self.name,
|
|
392
|
+
source_type=self.source_type,
|
|
393
|
+
data=meeting_context,
|
|
394
|
+
raw_text="",
|
|
395
|
+
metadata={"task_id": task_id, "meeting_count": 0},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
raw_text = self._format_meeting_context(meeting_context)
|
|
399
|
+
|
|
400
|
+
return SourceContext(
|
|
401
|
+
source_name=self.name,
|
|
402
|
+
source_type=self.source_type,
|
|
403
|
+
data=meeting_context,
|
|
404
|
+
raw_text=raw_text,
|
|
405
|
+
metadata={
|
|
406
|
+
"task_id": task_id,
|
|
407
|
+
"meeting_count": len(meeting_context.meetings),
|
|
408
|
+
},
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def _format_meeting_context(self, meeting_context: MeetingContext) -> str:
|
|
412
|
+
"""Format meeting context as raw text for synthesis."""
|
|
413
|
+
parts: list[str] = []
|
|
414
|
+
|
|
415
|
+
for meeting in meeting_context.meetings:
|
|
416
|
+
content_parts = [f"## {meeting.meeting_title}"]
|
|
417
|
+
content_parts.append(f"\n**Date:** {meeting.meeting_date.strftime('%Y-%m-%d')}")
|
|
418
|
+
|
|
419
|
+
if meeting.participants:
|
|
420
|
+
content_parts.append(f"**Participants:** {', '.join(meeting.participants)}")
|
|
421
|
+
|
|
422
|
+
content_parts.append(f"\n### Relevant Discussion\n\n{meeting.excerpt}")
|
|
423
|
+
|
|
424
|
+
if meeting.action_items:
|
|
425
|
+
content_parts.append("\n### Action Items")
|
|
426
|
+
for item in meeting.action_items:
|
|
427
|
+
content_parts.append(f"- {item}")
|
|
428
|
+
|
|
429
|
+
if meeting.decisions:
|
|
430
|
+
content_parts.append("\n### Decisions")
|
|
431
|
+
for decision in meeting.decisions:
|
|
432
|
+
content_parts.append(f"- {decision}")
|
|
433
|
+
|
|
434
|
+
parts.append("\n".join(content_parts))
|
|
435
|
+
|
|
436
|
+
return "\n\n---\n\n".join(parts)
|
|
437
|
+
|
|
438
|
+
async def search(
|
|
439
|
+
self,
|
|
440
|
+
query: str,
|
|
441
|
+
max_results: int = 10,
|
|
442
|
+
) -> list[SearchResult]:
|
|
443
|
+
"""Search Fireflies transcripts for items matching the query.
|
|
444
|
+
|
|
445
|
+
Implements the Adapter interface.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
query: Search terms to find in transcripts.
|
|
449
|
+
max_results: Maximum number of results to return.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
List of SearchResult items.
|
|
453
|
+
"""
|
|
454
|
+
if not self._config.enabled:
|
|
455
|
+
return []
|
|
456
|
+
|
|
457
|
+
transcripts = await self.search_transcripts(query)
|
|
458
|
+
|
|
459
|
+
results: list[SearchResult] = []
|
|
460
|
+
for transcript in transcripts[:max_results]:
|
|
461
|
+
title = transcript.get("title") or "Untitled Meeting"
|
|
462
|
+
date_str = transcript.get("date", "")
|
|
463
|
+
|
|
464
|
+
# Get overview from summary
|
|
465
|
+
summary = transcript.get("summary") or {}
|
|
466
|
+
excerpt = summary.get("overview") or ""
|
|
467
|
+
if not excerpt:
|
|
468
|
+
# Fall back to first few sentences
|
|
469
|
+
sentences = transcript.get("sentences") or []
|
|
470
|
+
if sentences:
|
|
471
|
+
excerpt = " ".join(s.get("text", "") for s in sentences[:3])
|
|
472
|
+
|
|
473
|
+
results.append(
|
|
474
|
+
SearchResult(
|
|
475
|
+
source_name=self.name,
|
|
476
|
+
source_type=self.source_type,
|
|
477
|
+
title=title,
|
|
478
|
+
excerpt=excerpt[:500] if excerpt else "No excerpt available",
|
|
479
|
+
metadata={
|
|
480
|
+
"date": date_str,
|
|
481
|
+
"participants": transcript.get("participants") or [],
|
|
482
|
+
},
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return results
|
|
487
|
+
|
|
488
|
+
async def fetch_context(self, task_id: str) -> list[ContextData]:
|
|
489
|
+
"""Fetch context from Fireflies (legacy Adapter interface).
|
|
490
|
+
|
|
491
|
+
This method is kept for backward compatibility.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
task_id: The task identifier to search for in transcripts.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
List of ContextData items, one per relevant meeting.
|
|
498
|
+
"""
|
|
499
|
+
source_context = await self.fetch_task_context(task_id)
|
|
500
|
+
|
|
501
|
+
if source_context.is_empty():
|
|
502
|
+
return []
|
|
503
|
+
|
|
504
|
+
meeting_context = source_context.data
|
|
505
|
+
if not isinstance(meeting_context, MeetingContext):
|
|
506
|
+
return []
|
|
507
|
+
|
|
508
|
+
# Convert each meeting excerpt to ContextData
|
|
509
|
+
results: list[ContextData] = []
|
|
510
|
+
for meeting in meeting_context.meetings:
|
|
511
|
+
content_parts = [f"## {meeting.meeting_title}"]
|
|
512
|
+
content_parts.append(f"\n**Date:** {meeting.meeting_date.strftime('%Y-%m-%d')}")
|
|
513
|
+
|
|
514
|
+
if meeting.participants:
|
|
515
|
+
content_parts.append(f"**Participants:** {', '.join(meeting.participants)}")
|
|
516
|
+
|
|
517
|
+
content_parts.append(f"\n### Relevant Discussion\n\n{meeting.excerpt}")
|
|
518
|
+
|
|
519
|
+
if meeting.action_items:
|
|
520
|
+
content_parts.append("\n### Action Items")
|
|
521
|
+
for item in meeting.action_items:
|
|
522
|
+
content_parts.append(f"- {item}")
|
|
523
|
+
|
|
524
|
+
if meeting.decisions:
|
|
525
|
+
content_parts.append("\n### Decisions")
|
|
526
|
+
for decision in meeting.decisions:
|
|
527
|
+
content_parts.append(f"- {decision}")
|
|
528
|
+
|
|
529
|
+
content = "\n".join(content_parts)
|
|
530
|
+
|
|
531
|
+
results.append(
|
|
532
|
+
ContextData(
|
|
533
|
+
source=f"fireflies:{meeting.meeting_date.strftime('%Y-%m-%d')}",
|
|
534
|
+
source_type=self.source_type,
|
|
535
|
+
title=meeting.meeting_title,
|
|
536
|
+
content=content,
|
|
537
|
+
metadata={
|
|
538
|
+
"date": meeting.meeting_date.isoformat(),
|
|
539
|
+
"participants": meeting.participants,
|
|
540
|
+
"action_item_count": len(meeting.action_items),
|
|
541
|
+
},
|
|
542
|
+
relevance_score=0.8,
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
return results
|
|
547
|
+
|
|
548
|
+
async def health_check(self) -> bool:
|
|
549
|
+
"""Check if Fireflies is configured and accessible.
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
True if healthy or disabled, False if there's an issue.
|
|
553
|
+
"""
|
|
554
|
+
if not self._config.enabled:
|
|
555
|
+
return True
|
|
556
|
+
|
|
557
|
+
if not self._config.api_key:
|
|
558
|
+
logger.warning("Fireflies adapter missing API key")
|
|
559
|
+
return False
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
# Simple query to verify API access
|
|
563
|
+
client = self._get_client()
|
|
564
|
+
response = await client.post(
|
|
565
|
+
FIREFLIES_API_URL,
|
|
566
|
+
json={
|
|
567
|
+
"query": "query { user { email } }",
|
|
568
|
+
},
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
if response.status_code == 200:
|
|
572
|
+
data = response.json()
|
|
573
|
+
if "errors" not in data:
|
|
574
|
+
logger.info("Fireflies health check passed")
|
|
575
|
+
return True
|
|
576
|
+
|
|
577
|
+
logger.warning(
|
|
578
|
+
"Fireflies health check failed",
|
|
579
|
+
extra={"status_code": response.status_code},
|
|
580
|
+
)
|
|
581
|
+
return False
|
|
582
|
+
|
|
583
|
+
except Exception as e:
|
|
584
|
+
logger.warning("Fireflies health check error", extra={"error": str(e)})
|
|
585
|
+
return False
|