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.
@@ -0,0 +1,321 @@
1
+ """Base plugin interfaces for DevsContext.
2
+
3
+ This module defines the abstract base classes for the plugin system:
4
+
5
+ - Adapter: Interface for data sources (Jira, Fireflies, Slack, etc.)
6
+ - SynthesisPlugin: Interface for synthesis strategies (LLM, template, etc.)
7
+
8
+ Adapters and plugins must implement these interfaces to integrate with DevsContext.
9
+ The core system uses these interfaces to fetch, search, and synthesize context
10
+ without knowing the specific implementation details.
11
+
12
+ Design Principles:
13
+ - All I/O operations are async
14
+ - Adapters never raise exceptions that crash the MCP server
15
+ - Adapters handle their own resource lifecycle (HTTP clients, etc.)
16
+ - Config validation is done via Pydantic models
17
+ - Graceful degradation: if a source fails, others continue
18
+
19
+ Example Adapter:
20
+ class SlackAdapter(Adapter):
21
+ name = "slack"
22
+ source_type = "communication"
23
+ config_schema = SlackConfig
24
+
25
+ def __init__(self, config: SlackConfig):
26
+ self._config = config
27
+ self._client = None
28
+
29
+ async def fetch_task_context(self, task_id, ticket=None):
30
+ messages = await self._search_messages(task_id)
31
+ return SourceContext(
32
+ source_name="slack",
33
+ source_type="communication",
34
+ data={"messages": messages},
35
+ raw_text=self._format_messages(messages),
36
+ )
37
+
38
+ Example Synthesis Plugin:
39
+ class TemplateSynthesis(SynthesisPlugin):
40
+ name = "template"
41
+ config_schema = TemplateConfig
42
+
43
+ async def synthesize(self, task_id, source_contexts):
44
+ return self._template.render(task_id=task_id, **source_contexts)
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ from abc import ABC, abstractmethod
50
+ from datetime import datetime
51
+ from typing import TYPE_CHECKING, Any, ClassVar
52
+
53
+ from pydantic import BaseModel, Field
54
+
55
+ if TYPE_CHECKING:
56
+ from devscontext.models import JiraTicket
57
+
58
+
59
+ # =============================================================================
60
+ # DATA MODELS
61
+ # =============================================================================
62
+
63
+
64
+ class SourceContext(BaseModel):
65
+ """Container for context data returned by a source plugin.
66
+
67
+ This is a flexible container that can hold any type of source-specific data.
68
+ The `data` field holds structured data (dicts, lists, Pydantic models),
69
+ while `raw_text` provides a pre-formatted text representation for synthesis.
70
+
71
+ Attributes:
72
+ source_name: The plugin name that produced this context (e.g., "jira").
73
+ source_type: Category of the source (e.g., "issue_tracker", "meeting").
74
+ data: The structured source-specific data. Can be any type.
75
+ raw_text: Pre-formatted text representation of the data for synthesis.
76
+ metadata: Additional metadata about the context (timestamps, counts, etc.).
77
+ fetched_at: When this context was fetched (UTC).
78
+ """
79
+
80
+ source_name: str = Field(..., description="Plugin name that produced this context")
81
+ source_type: str = Field(..., description="Category of source (issue_tracker, meeting, etc.)")
82
+ data: Any = Field(default=None, description="Structured source-specific data")
83
+ raw_text: str = Field(default="", description="Pre-formatted text for synthesis")
84
+ metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
85
+ fetched_at: datetime = Field(
86
+ default_factory=lambda: datetime.now().astimezone(),
87
+ description="When context was fetched (UTC)",
88
+ )
89
+
90
+ model_config = {"arbitrary_types_allowed": True}
91
+
92
+ def is_empty(self) -> bool:
93
+ """Check if this context has no meaningful data."""
94
+ return not self.data and not self.raw_text
95
+
96
+
97
+ class SearchResult(BaseModel):
98
+ """A single search result from a source plugin.
99
+
100
+ Search results are returned by SourcePlugin.search() and represent
101
+ a single item that matched the search query.
102
+
103
+ Attributes:
104
+ source_name: The plugin name that produced this result.
105
+ source_type: Category of the source.
106
+ title: Human-readable title for the result.
107
+ excerpt: Relevant excerpt or snippet showing the match.
108
+ url: Optional URL to the source item.
109
+ relevance_score: Score from 0.0 to 1.0 indicating match quality.
110
+ metadata: Additional source-specific metadata.
111
+ """
112
+
113
+ source_name: str = Field(..., description="Plugin name that produced this result")
114
+ source_type: str = Field(..., description="Category of source")
115
+ title: str = Field(..., description="Human-readable title")
116
+ excerpt: str = Field(..., description="Relevant excerpt showing the match")
117
+ url: str | None = Field(default=None, description="URL to the source item")
118
+ relevance_score: float = Field(
119
+ default=1.0,
120
+ ge=0.0,
121
+ le=1.0,
122
+ description="Relevance score from 0.0 to 1.0",
123
+ )
124
+ metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
125
+
126
+
127
+ # =============================================================================
128
+ # ADAPTER INTERFACE
129
+ # =============================================================================
130
+
131
+
132
+ class Adapter(ABC):
133
+ """Abstract base class for all context adapters.
134
+
135
+ An adapter provides context data from a specific system (Jira, Slack,
136
+ Fireflies, local docs, etc.). Adapters must implement the interface methods
137
+ to integrate with DevsContext.
138
+
139
+ Class Attributes:
140
+ name: Unique identifier for this adapter (e.g., "jira", "slack").
141
+ source_type: Category of source (e.g., "issue_tracker", "communication").
142
+ config_schema: Pydantic model class for validating adapter configuration.
143
+
144
+ Implementation Requirements:
145
+ - All I/O must be async
146
+ - Never raise exceptions that crash the server - log and return empty
147
+ - Handle resource lifecycle (create/close HTTP clients)
148
+ - Support graceful degradation when dependencies unavailable
149
+
150
+ Example:
151
+ class MyAdapter(Adapter):
152
+ name = "my_source"
153
+ source_type = "custom"
154
+ config_schema = MyConfig
155
+
156
+ def __init__(self, config: MyConfig):
157
+ self._config = config
158
+
159
+ async def fetch_task_context(self, task_id, ticket=None):
160
+ data = await self._fetch_data(task_id)
161
+ return SourceContext(
162
+ source_name=self.name,
163
+ source_type=self.source_type,
164
+ data=data,
165
+ raw_text=self._format_data(data),
166
+ )
167
+ """
168
+
169
+ # Class attributes that subclasses must define
170
+ name: ClassVar[str]
171
+ source_type: ClassVar[str]
172
+ config_schema: ClassVar[type[BaseModel]]
173
+
174
+ @abstractmethod
175
+ async def fetch_task_context(
176
+ self,
177
+ task_id: str,
178
+ ticket: JiraTicket | None = None,
179
+ ) -> SourceContext:
180
+ """Fetch context related to a specific task.
181
+
182
+ This is the primary method for getting context. The task_id is typically
183
+ a Jira ticket ID, but adapters can interpret it as needed. The optional
184
+ ticket parameter provides Jira ticket data for enriched matching (e.g.,
185
+ using ticket components/labels to find relevant docs).
186
+
187
+ Args:
188
+ task_id: The task identifier (e.g., "PROJ-123").
189
+ ticket: Optional Jira ticket data for context-aware matching.
190
+ Non-Jira adapters use this for keyword extraction, etc.
191
+
192
+ Returns:
193
+ SourceContext with the fetched data. Return empty context on errors.
194
+ """
195
+ ...
196
+
197
+ @abstractmethod
198
+ async def search(
199
+ self,
200
+ query: str,
201
+ max_results: int = 10,
202
+ ) -> list[SearchResult]:
203
+ """Search this source for items matching the query.
204
+
205
+ Used by the search_context MCP tool for freeform keyword search.
206
+ This should be a fast, lightweight search - no LLM processing.
207
+
208
+ Args:
209
+ query: The search query string.
210
+ max_results: Maximum number of results to return.
211
+
212
+ Returns:
213
+ List of SearchResult items. Empty list if nothing found or on error.
214
+ """
215
+ ...
216
+
217
+ @abstractmethod
218
+ async def health_check(self) -> bool:
219
+ """Check if the adapter is properly configured and can connect.
220
+
221
+ Should verify:
222
+ - Configuration is valid
223
+ - External service is reachable (if applicable)
224
+ - Authentication is working
225
+
226
+ Returns:
227
+ True if healthy, False if there's an issue.
228
+ """
229
+ ...
230
+
231
+ async def close(self) -> None: # noqa: B027
232
+ """Clean up resources (HTTP clients, connections, etc.).
233
+
234
+ Override this method if your adapter holds resources that need cleanup.
235
+ The default implementation does nothing.
236
+ """
237
+ pass
238
+
239
+ def format_for_synthesis(self, context: SourceContext) -> str:
240
+ """Format context data for LLM synthesis.
241
+
242
+ Override this to provide custom formatting. The default implementation
243
+ returns the raw_text field from the context.
244
+
245
+ Args:
246
+ context: The source context to format.
247
+
248
+ Returns:
249
+ Formatted text string for inclusion in synthesis prompt.
250
+ """
251
+ return context.raw_text
252
+
253
+
254
+ # =============================================================================
255
+ # SYNTHESIS PLUGIN INTERFACE
256
+ # =============================================================================
257
+
258
+
259
+ class SynthesisPlugin(ABC):
260
+ """Abstract base class for synthesis plugins.
261
+
262
+ A synthesis plugin combines context from multiple adapters into a unified,
263
+ structured output suitable for AI coding assistants. Different strategies
264
+ can be implemented: LLM-based synthesis, template-based, or passthrough.
265
+
266
+ Class Attributes:
267
+ name: Unique identifier for this plugin (e.g., "llm", "template").
268
+ config_schema: Pydantic model class for validating plugin configuration.
269
+
270
+ Implementation Requirements:
271
+ - Must be async
272
+ - Should handle missing sources gracefully
273
+ - Should provide fallback if primary synthesis fails
274
+
275
+ Example:
276
+ class TemplateSynthesis(SynthesisPlugin):
277
+ name = "template"
278
+ config_schema = TemplateConfig
279
+
280
+ def __init__(self, config: TemplateConfig):
281
+ self._template = load_template(config.template_path)
282
+
283
+ async def synthesize(self, task_id, source_contexts):
284
+ return self._template.render(
285
+ task_id=task_id,
286
+ contexts=source_contexts,
287
+ )
288
+ """
289
+
290
+ # Class attributes that subclasses must define
291
+ name: ClassVar[str]
292
+ config_schema: ClassVar[type[BaseModel]]
293
+
294
+ @abstractmethod
295
+ async def synthesize(
296
+ self,
297
+ task_id: str,
298
+ source_contexts: dict[str, SourceContext],
299
+ ) -> str:
300
+ """Synthesize context from multiple adapters into unified output.
301
+
302
+ Takes context data from all enabled adapters and combines it
303
+ into a structured markdown document suitable for AI coding assistants.
304
+
305
+ Args:
306
+ task_id: The task identifier (e.g., "PROJ-123").
307
+ source_contexts: Dict mapping adapter names to their SourceContext.
308
+ Keys are adapter names (e.g., "jira", "fireflies").
309
+
310
+ Returns:
311
+ Synthesized markdown text combining all source context.
312
+ """
313
+ ...
314
+
315
+ async def close(self) -> None: # noqa: B027
316
+ """Clean up resources (LLM clients, etc.).
317
+
318
+ Override this method if your plugin holds resources that need cleanup.
319
+ The default implementation does nothing.
320
+ """
321
+ pass