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,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
|