|
3 | 3 | This adapter connects to the Fireflies.ai GraphQL API to fetch meeting |
4 | 4 | transcripts and search for relevant discussions related to a task. |
5 | 5 |
|
| 6 | +This adapter implements the Adapter interface for the plugin system. |
| 7 | +
|
6 | 8 | Example: |
7 | 9 | config = FirefliesConfig(api_key="your-api-key", enabled=True) |
8 | 10 | adapter = FirefliesAdapter(config) |
9 | | - context = await adapter.fetch_context("PROJ-123") |
| 11 | + context = await adapter.fetch_task_context("PROJ-123") |
10 | 12 | """ |
11 | 13 |
|
12 | 14 | from __future__ import annotations |
13 | 15 |
|
14 | 16 | import re |
15 | 17 | from datetime import UTC, datetime |
16 | | -from typing import TYPE_CHECKING, Any |
| 18 | +from typing import TYPE_CHECKING, Any, ClassVar |
17 | 19 |
|
18 | 20 | import httpx |
19 | 21 |
|
20 | | -from devscontext.adapters.base import Adapter |
21 | 22 | from devscontext.constants import ( |
22 | 23 | ADAPTER_FIREFLIES, |
23 | 24 | DEFAULT_HTTP_TIMEOUT_SECONDS, |
|
27 | 28 | SOURCE_TYPE_MEETING, |
28 | 29 | ) |
29 | 30 | from devscontext.logging import get_logger |
30 | | -from devscontext.models import ContextData, MeetingContext, MeetingExcerpt |
| 31 | +from devscontext.models import ContextData, FirefliesConfig, MeetingContext, MeetingExcerpt |
| 32 | +from devscontext.plugins.base import Adapter, SearchResult, SourceContext |
31 | 33 |
|
32 | 34 | if TYPE_CHECKING: |
33 | | - from devscontext.models import FirefliesConfig |
| 35 | + from devscontext.models import JiraTicket |
34 | 36 |
|
35 | 37 | logger = get_logger(__name__) |
36 | 38 |
|
|
59 | 61 | class FirefliesAdapter(Adapter): |
60 | 62 | """Adapter for fetching context from Fireflies meeting transcripts. |
61 | 63 |
|
62 | | - This adapter connects to Fireflies.ai to search for meeting transcripts |
| 64 | + Implements the Adapter interface for the plugin system. |
| 65 | + Connects to Fireflies.ai to search for meeting transcripts |
63 | 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). |
64 | 72 | """ |
65 | 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 | + |
66 | 79 | def __init__(self, config: FirefliesConfig) -> None: |
67 | | - """Initialize the Fireflies adapter.""" |
| 80 | + """Initialize the Fireflies adapter. |
| 81 | +
|
| 82 | + Args: |
| 83 | + config: Fireflies configuration with API key. |
| 84 | + """ |
68 | 85 | self._config = config |
69 | 86 | self._client: httpx.AsyncClient | None = None |
70 | 87 |
|
71 | | - @property |
72 | | - def name(self) -> str: |
73 | | - """Return the adapter name.""" |
74 | | - return ADAPTER_FIREFLIES |
75 | | - |
76 | | - @property |
77 | | - def source_type(self) -> str: |
78 | | - """Return the source type.""" |
79 | | - return SOURCE_TYPE_MEETING |
80 | | - |
81 | 88 | def _get_client(self) -> httpx.AsyncClient: |
82 | 89 | """Get or create the HTTP client.""" |
83 | 90 | if self._client is None: |
@@ -351,28 +358,156 @@ async def get_meeting_context(self, task_id: str) -> MeetingContext: |
351 | 358 |
|
352 | 359 | return MeetingContext(meetings=excerpts) |
353 | 360 |
|
354 | | - async def fetch_context(self, task_id: str) -> list[ContextData]: |
| 361 | + async def fetch_task_context( |
| 362 | + self, |
| 363 | + task_id: str, |
| 364 | + ticket: JiraTicket | None = None, |
| 365 | + ) -> SourceContext: |
355 | 366 | """Fetch context from Fireflies meeting transcripts. |
356 | 367 |
|
| 368 | + Implements the Adapter interface. Searches for meetings mentioning |
| 369 | + the task ID or keywords from the ticket. |
| 370 | +
|
357 | 371 | Args: |
358 | 372 | task_id: The task identifier to search for in transcripts. |
| 373 | + ticket: Optional Jira ticket for keyword extraction. |
359 | 374 |
|
360 | 375 | Returns: |
361 | | - List of ContextData items, one per relevant meeting. |
| 376 | + SourceContext with MeetingContext data. |
362 | 377 | """ |
363 | 378 | if not self._config.enabled: |
364 | 379 | logger.debug("Fireflies adapter is disabled") |
365 | | - return [] |
| 380 | + return SourceContext( |
| 381 | + source_name=self.name, |
| 382 | + source_type=self.source_type, |
| 383 | + data=None, |
| 384 | + raw_text="", |
| 385 | + ) |
366 | 386 |
|
367 | 387 | meeting_context = await self.get_meeting_context(task_id) |
368 | 388 |
|
369 | 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): |
370 | 506 | return [] |
371 | 507 |
|
372 | 508 | # Convert each meeting excerpt to ContextData |
373 | 509 | results: list[ContextData] = [] |
374 | 510 | for meeting in meeting_context.meetings: |
375 | | - # Format content |
376 | 511 | content_parts = [f"## {meeting.meeting_title}"] |
377 | 512 | content_parts.append(f"\n**Date:** {meeting.meeting_date.strftime('%Y-%m-%d')}") |
378 | 513 |
|
|
0 commit comments