Skip to content

Commit 41e2e15

Browse files
committed
feat: add plugin architecture with registry and synthesis plugins
1 parent dc77591 commit 41e2e15

13 files changed

Lines changed: 2159 additions & 363 deletions

File tree

src/devscontext/adapters/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
- FirefliesAdapter: Meeting transcripts from Fireflies.ai
66
- LocalDocsAdapter: Local markdown documentation
77
8-
All adapters implement the Adapter base class interface.
8+
All adapters implement the Adapter interface from the plugins module.
99
"""
1010

11-
from devscontext.adapters.base import Adapter
1211
from devscontext.adapters.fireflies import FirefliesAdapter
1312
from devscontext.adapters.jira import JiraAdapter
1413
from devscontext.adapters.local_docs import LocalDocsAdapter
1514
from devscontext.models import ContextData
15+
from devscontext.plugins.base import Adapter
1616

1717
__all__ = [
1818
"Adapter",

src/devscontext/adapters/fireflies.py

Lines changed: 156 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,22 @@
33
This adapter connects to the Fireflies.ai GraphQL API to fetch meeting
44
transcripts and search for relevant discussions related to a task.
55
6+
This adapter implements the Adapter interface for the plugin system.
7+
68
Example:
79
config = FirefliesConfig(api_key="your-api-key", enabled=True)
810
adapter = FirefliesAdapter(config)
9-
context = await adapter.fetch_context("PROJ-123")
11+
context = await adapter.fetch_task_context("PROJ-123")
1012
"""
1113

1214
from __future__ import annotations
1315

1416
import re
1517
from datetime import UTC, datetime
16-
from typing import TYPE_CHECKING, Any
18+
from typing import TYPE_CHECKING, Any, ClassVar
1719

1820
import httpx
1921

20-
from devscontext.adapters.base import Adapter
2122
from devscontext.constants import (
2223
ADAPTER_FIREFLIES,
2324
DEFAULT_HTTP_TIMEOUT_SECONDS,
@@ -27,10 +28,11 @@
2728
SOURCE_TYPE_MEETING,
2829
)
2930
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
3133

3234
if TYPE_CHECKING:
33-
from devscontext.models import FirefliesConfig
35+
from devscontext.models import JiraTicket
3436

3537
logger = get_logger(__name__)
3638

@@ -59,25 +61,30 @@
5961
class FirefliesAdapter(Adapter):
6062
"""Adapter for fetching context from Fireflies meeting transcripts.
6163
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
6366
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).
6472
"""
6573

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+
6679
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+
"""
6885
self._config = config
6986
self._client: httpx.AsyncClient | None = None
7087

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-
8188
def _get_client(self) -> httpx.AsyncClient:
8289
"""Get or create the HTTP client."""
8390
if self._client is None:
@@ -351,28 +358,156 @@ async def get_meeting_context(self, task_id: str) -> MeetingContext:
351358

352359
return MeetingContext(meetings=excerpts)
353360

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:
355366
"""Fetch context from Fireflies meeting transcripts.
356367
368+
Implements the Adapter interface. Searches for meetings mentioning
369+
the task ID or keywords from the ticket.
370+
357371
Args:
358372
task_id: The task identifier to search for in transcripts.
373+
ticket: Optional Jira ticket for keyword extraction.
359374
360375
Returns:
361-
List of ContextData items, one per relevant meeting.
376+
SourceContext with MeetingContext data.
362377
"""
363378
if not self._config.enabled:
364379
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+
)
366386

367387
meeting_context = await self.get_meeting_context(task_id)
368388

369389
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):
370506
return []
371507

372508
# Convert each meeting excerpt to ContextData
373509
results: list[ContextData] = []
374510
for meeting in meeting_context.meetings:
375-
# Format content
376511
content_parts = [f"## {meeting.meeting_title}"]
377512
content_parts.append(f"\n**Date:** {meeting.meeting_date.strftime('%Y-%m-%d')}")
378513

0 commit comments

Comments
 (0)