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
devscontext/models.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"""Pydantic models for DevsContext.
|
|
2
|
+
|
|
3
|
+
This module contains all data models used throughout DevsContext.
|
|
4
|
+
All models use Pydantic BaseModel with Field() descriptions for
|
|
5
|
+
documentation and validation.
|
|
6
|
+
|
|
7
|
+
Models are organized by domain:
|
|
8
|
+
- Config models (JiraConfig, FirefliesConfig, DocsConfig, etc.)
|
|
9
|
+
- Jira data models (JiraTicket, JiraComment, LinkedIssue, JiraContext)
|
|
10
|
+
- Meeting data models (MeetingExcerpt, MeetingContext)
|
|
11
|
+
- Documentation models (DocSection, DocsContext)
|
|
12
|
+
- Result models (TaskContext, ContextData)
|
|
13
|
+
|
|
14
|
+
All datetime fields use timezone-aware UTC datetimes.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from datetime import UTC, datetime
|
|
20
|
+
from typing import Any, Literal
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# CONFIG MODELS
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class JiraConfig(BaseModel):
|
|
30
|
+
"""Jira adapter configuration."""
|
|
31
|
+
|
|
32
|
+
base_url: str = Field(default="", description="Jira instance URL")
|
|
33
|
+
email: str = Field(default="", description="Jira authentication email")
|
|
34
|
+
api_token: str = Field(default="", description="Jira API token (from env)")
|
|
35
|
+
project: str = Field(default="", description="Default Jira project key")
|
|
36
|
+
enabled: bool = Field(default=False, description="Whether adapter is enabled")
|
|
37
|
+
primary: bool = Field(
|
|
38
|
+
default=True,
|
|
39
|
+
description="Primary sources are fetched first, context shared with secondary sources",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FirefliesConfig(BaseModel):
|
|
44
|
+
"""Fireflies.ai adapter configuration."""
|
|
45
|
+
|
|
46
|
+
api_key: str = Field(default="", description="Fireflies.ai API key (from env)")
|
|
47
|
+
enabled: bool = Field(default=False, description="Whether adapter is enabled")
|
|
48
|
+
primary: bool = Field(default=False, description="Whether this is a primary source")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RagConfig(BaseModel):
|
|
52
|
+
"""RAG configuration for embedding-based doc search.
|
|
53
|
+
|
|
54
|
+
When enabled, the LocalDocsAdapter uses semantic similarity (embeddings)
|
|
55
|
+
instead of keyword matching for finding relevant documentation sections.
|
|
56
|
+
|
|
57
|
+
Requires: pip install devscontext[rag]
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
enabled: bool = Field(default=False, description="Enable RAG for doc matching")
|
|
61
|
+
embedding_provider: Literal["local", "openai", "ollama"] = Field(
|
|
62
|
+
default="local",
|
|
63
|
+
description="Embedding provider (local=sentence-transformers, openai, ollama)",
|
|
64
|
+
)
|
|
65
|
+
embedding_model: str = Field(
|
|
66
|
+
default="all-MiniLM-L6-v2",
|
|
67
|
+
description="Model for generating embeddings",
|
|
68
|
+
)
|
|
69
|
+
index_path: str = Field(
|
|
70
|
+
default=".devscontext/doc_index.json",
|
|
71
|
+
description="Path to the embedding index file",
|
|
72
|
+
)
|
|
73
|
+
top_k: int = Field(
|
|
74
|
+
default=10,
|
|
75
|
+
ge=1,
|
|
76
|
+
le=50,
|
|
77
|
+
description="Number of similar sections to retrieve",
|
|
78
|
+
)
|
|
79
|
+
similarity_threshold: float = Field(
|
|
80
|
+
default=0.3,
|
|
81
|
+
ge=0.0,
|
|
82
|
+
le=1.0,
|
|
83
|
+
description="Minimum similarity score (0-1) for results",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class DocsConfig(BaseModel):
|
|
88
|
+
"""Local documentation adapter configuration."""
|
|
89
|
+
|
|
90
|
+
paths: list[str] = Field(
|
|
91
|
+
default_factory=lambda: ["./docs/"],
|
|
92
|
+
description="Paths to documentation directories",
|
|
93
|
+
)
|
|
94
|
+
standards_path: str | None = Field(default=None, description="Path to coding standards docs")
|
|
95
|
+
architecture_path: str | None = Field(default=None, description="Path to architecture docs")
|
|
96
|
+
enabled: bool = Field(default=True, description="Whether adapter is enabled")
|
|
97
|
+
primary: bool = Field(default=False, description="Whether this is a primary source")
|
|
98
|
+
rag: RagConfig | None = Field(default=None, description="Optional RAG configuration")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SlackConfig(BaseModel):
|
|
102
|
+
"""Slack adapter configuration."""
|
|
103
|
+
|
|
104
|
+
bot_token: str = Field(default="", description="Slack bot token (from env)")
|
|
105
|
+
channels: list[str] = Field(
|
|
106
|
+
default_factory=list,
|
|
107
|
+
description="Channel names to search (e.g., ['engineering', 'payments-team'])",
|
|
108
|
+
)
|
|
109
|
+
include_threads: bool = Field(default=True, description="Fetch full threads for matches")
|
|
110
|
+
max_messages: int = Field(
|
|
111
|
+
default=20,
|
|
112
|
+
ge=1,
|
|
113
|
+
le=100,
|
|
114
|
+
description="Max messages to return per search",
|
|
115
|
+
)
|
|
116
|
+
lookback_days: int = Field(
|
|
117
|
+
default=30,
|
|
118
|
+
ge=1,
|
|
119
|
+
le=90,
|
|
120
|
+
description="Days to look back when searching",
|
|
121
|
+
)
|
|
122
|
+
enabled: bool = Field(default=False, description="Whether adapter is enabled")
|
|
123
|
+
primary: bool = Field(default=False, description="Whether this is a primary source")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class GmailConfig(BaseModel):
|
|
127
|
+
"""Gmail adapter configuration."""
|
|
128
|
+
|
|
129
|
+
credentials_path: str = Field(
|
|
130
|
+
default="",
|
|
131
|
+
description="Path to OAuth2 credentials JSON (from env)",
|
|
132
|
+
)
|
|
133
|
+
token_path: str = Field(
|
|
134
|
+
default=".devscontext/gmail_token.json",
|
|
135
|
+
description="Path to store OAuth2 refresh token",
|
|
136
|
+
)
|
|
137
|
+
search_scope: str = Field(
|
|
138
|
+
default="newer_than:30d",
|
|
139
|
+
description="Gmail search scope filter",
|
|
140
|
+
)
|
|
141
|
+
max_results: int = Field(
|
|
142
|
+
default=10,
|
|
143
|
+
ge=1,
|
|
144
|
+
le=50,
|
|
145
|
+
description="Max emails to return",
|
|
146
|
+
)
|
|
147
|
+
labels: list[str] = Field(
|
|
148
|
+
default_factory=lambda: ["INBOX"],
|
|
149
|
+
description="Labels to search within",
|
|
150
|
+
)
|
|
151
|
+
enabled: bool = Field(default=False, description="Whether adapter is enabled")
|
|
152
|
+
primary: bool = Field(default=False, description="Whether this is a primary source")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SynthesisConfig(BaseModel):
|
|
156
|
+
"""Synthesis configuration supporting multiple synthesis plugins."""
|
|
157
|
+
|
|
158
|
+
plugin: Literal["llm", "template", "passthrough"] = Field(
|
|
159
|
+
default="llm",
|
|
160
|
+
description="Synthesis plugin to use (llm, template, passthrough)",
|
|
161
|
+
)
|
|
162
|
+
provider: Literal["anthropic", "openai", "ollama"] = Field(
|
|
163
|
+
default="anthropic",
|
|
164
|
+
description="LLM provider for synthesis (only used when plugin=llm)",
|
|
165
|
+
)
|
|
166
|
+
model: str = Field(default="claude-haiku-4-5", description="Model name/ID to use")
|
|
167
|
+
api_key: str | None = Field(default=None, description="API key for the provider (from env)")
|
|
168
|
+
max_output_tokens: int = Field(
|
|
169
|
+
default=3000,
|
|
170
|
+
ge=100,
|
|
171
|
+
le=10000,
|
|
172
|
+
description="Maximum tokens in synthesized output",
|
|
173
|
+
)
|
|
174
|
+
temperature: float = Field(
|
|
175
|
+
default=0.0,
|
|
176
|
+
ge=0.0,
|
|
177
|
+
le=2.0,
|
|
178
|
+
description="Temperature for LLM generation",
|
|
179
|
+
)
|
|
180
|
+
prompt_template: str | None = Field(
|
|
181
|
+
default=None,
|
|
182
|
+
description="Path to custom prompt template file (optional)",
|
|
183
|
+
)
|
|
184
|
+
template_path: str | None = Field(
|
|
185
|
+
default=None,
|
|
186
|
+
description="Path to Jinja2 template (only used when plugin=template)",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class CacheConfig(BaseModel):
|
|
191
|
+
"""Cache configuration."""
|
|
192
|
+
|
|
193
|
+
enabled: bool = Field(default=True, description="Whether caching is enabled")
|
|
194
|
+
ttl_minutes: int = Field(default=15, ge=1, le=1440, description="Cache entry TTL in minutes")
|
|
195
|
+
max_size: int = Field(default=100, ge=1, description="Maximum cache entries")
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def ttl_seconds(self) -> int:
|
|
199
|
+
"""Return TTL in seconds for compatibility."""
|
|
200
|
+
return self.ttl_minutes * 60
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class AgentTriggerConfig(BaseModel):
|
|
204
|
+
"""Configuration for how the pre-processing agent is triggered."""
|
|
205
|
+
|
|
206
|
+
type: Literal["polling"] = Field(
|
|
207
|
+
default="polling",
|
|
208
|
+
description="Trigger type (polling or webhook in future)",
|
|
209
|
+
)
|
|
210
|
+
poll_interval_minutes: int = Field(
|
|
211
|
+
default=5,
|
|
212
|
+
ge=1,
|
|
213
|
+
le=60,
|
|
214
|
+
description="How often to poll Jira for ready tickets",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class PreprocessorConfig(BaseModel):
|
|
219
|
+
"""Configuration for the pre-processing agent."""
|
|
220
|
+
|
|
221
|
+
enabled: bool = Field(default=False, description="Whether agent is enabled")
|
|
222
|
+
trigger: AgentTriggerConfig = Field(default_factory=AgentTriggerConfig)
|
|
223
|
+
jira_status: str = Field(
|
|
224
|
+
default="Ready for Development",
|
|
225
|
+
description="Jira status that triggers pre-processing",
|
|
226
|
+
)
|
|
227
|
+
jira_project: str | list[str] = Field(
|
|
228
|
+
default="",
|
|
229
|
+
description="Project key(s) to watch (e.g., 'PROJ' or ['PROJ', 'TEAM'])",
|
|
230
|
+
)
|
|
231
|
+
context_ttl_hours: int = Field(
|
|
232
|
+
default=24,
|
|
233
|
+
ge=1,
|
|
234
|
+
le=168,
|
|
235
|
+
description="How long pre-built context is valid",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class AgentsConfig(BaseModel):
|
|
240
|
+
"""Configuration for background agents."""
|
|
241
|
+
|
|
242
|
+
preprocessor: PreprocessorConfig = Field(default_factory=PreprocessorConfig)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class StorageConfig(BaseModel):
|
|
246
|
+
"""Configuration for persistent storage."""
|
|
247
|
+
|
|
248
|
+
path: str = Field(
|
|
249
|
+
default=".devscontext/cache.db",
|
|
250
|
+
description="Path to SQLite database for pre-built context",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class SourcesConfig(BaseModel):
|
|
255
|
+
"""Configuration for all data sources."""
|
|
256
|
+
|
|
257
|
+
jira: JiraConfig = Field(default_factory=JiraConfig)
|
|
258
|
+
fireflies: FirefliesConfig = Field(default_factory=FirefliesConfig)
|
|
259
|
+
docs: DocsConfig = Field(default_factory=DocsConfig)
|
|
260
|
+
slack: SlackConfig = Field(default_factory=SlackConfig)
|
|
261
|
+
gmail: GmailConfig = Field(default_factory=GmailConfig)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class DevsContextConfig(BaseModel):
|
|
265
|
+
"""Root configuration for DevsContext."""
|
|
266
|
+
|
|
267
|
+
sources: SourcesConfig = Field(default_factory=SourcesConfig)
|
|
268
|
+
synthesis: SynthesisConfig = Field(default_factory=SynthesisConfig)
|
|
269
|
+
cache: CacheConfig = Field(default_factory=CacheConfig)
|
|
270
|
+
agents: AgentsConfig = Field(default_factory=AgentsConfig)
|
|
271
|
+
storage: StorageConfig = Field(default_factory=StorageConfig)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# =============================================================================
|
|
275
|
+
# JIRA DATA MODELS
|
|
276
|
+
# =============================================================================
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class JiraTicket(BaseModel):
|
|
280
|
+
"""A Jira ticket with its core fields."""
|
|
281
|
+
|
|
282
|
+
ticket_id: str = Field(..., description="Issue key (e.g., 'PROJ-123')")
|
|
283
|
+
title: str = Field(..., description="Issue summary/title")
|
|
284
|
+
description: str | None = Field(default=None, description="Issue description text")
|
|
285
|
+
status: str = Field(..., description="Current status (e.g., 'In Progress')")
|
|
286
|
+
assignee: str | None = Field(default=None, description="Assigned user's display name")
|
|
287
|
+
labels: list[str] = Field(default_factory=list, description="Labels on this issue")
|
|
288
|
+
components: list[str] = Field(default_factory=list, description="Components")
|
|
289
|
+
acceptance_criteria: str | None = Field(default=None, description="Acceptance criteria")
|
|
290
|
+
story_points: float | None = Field(default=None, ge=0, description="Story points estimate")
|
|
291
|
+
sprint: str | None = Field(default=None, description="Current sprint name")
|
|
292
|
+
created: datetime = Field(..., description="When issue was created (UTC)")
|
|
293
|
+
updated: datetime = Field(..., description="When issue was last updated (UTC)")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class JiraComment(BaseModel):
|
|
297
|
+
"""A comment on a Jira ticket."""
|
|
298
|
+
|
|
299
|
+
author: str = Field(..., description="Comment author's display name")
|
|
300
|
+
body: str = Field(..., description="Comment text content")
|
|
301
|
+
created: datetime = Field(..., description="When comment was created (UTC)")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class LinkedIssue(BaseModel):
|
|
305
|
+
"""A linked issue reference from a Jira ticket."""
|
|
306
|
+
|
|
307
|
+
ticket_id: str = Field(..., description="Issue key (e.g., 'PROJ-456')")
|
|
308
|
+
title: str = Field(..., description="Issue summary/title")
|
|
309
|
+
status: str = Field(..., description="Current status of linked issue")
|
|
310
|
+
link_type: str = Field(
|
|
311
|
+
...,
|
|
312
|
+
description="Link type (e.g., 'blocks', 'is blocked by', 'relates to')",
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class JiraContext(BaseModel):
|
|
317
|
+
"""Complete Jira context for a ticket."""
|
|
318
|
+
|
|
319
|
+
ticket: JiraTicket = Field(..., description="The main ticket details")
|
|
320
|
+
comments: list[JiraComment] = Field(default_factory=list, description="Comments")
|
|
321
|
+
linked_issues: list[LinkedIssue] = Field(default_factory=list, description="Linked issues")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# =============================================================================
|
|
325
|
+
# MEETING DATA MODELS
|
|
326
|
+
# =============================================================================
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class MeetingExcerpt(BaseModel):
|
|
330
|
+
"""Relevant excerpt from a meeting transcript."""
|
|
331
|
+
|
|
332
|
+
meeting_title: str = Field(..., description="Title of the meeting")
|
|
333
|
+
meeting_date: datetime = Field(..., description="When the meeting occurred (UTC)")
|
|
334
|
+
participants: list[str] = Field(default_factory=list, description="Participant names")
|
|
335
|
+
excerpt: str = Field(..., description="Relevant portion of transcript")
|
|
336
|
+
action_items: list[str] = Field(default_factory=list, description="Action items extracted")
|
|
337
|
+
decisions: list[str] = Field(default_factory=list, description="Decisions made")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class MeetingContext(BaseModel):
|
|
341
|
+
"""All meeting context found for a task."""
|
|
342
|
+
|
|
343
|
+
meetings: list[MeetingExcerpt] = Field(default_factory=list, description="Meeting excerpts")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# =============================================================================
|
|
347
|
+
# SLACK DATA MODELS
|
|
348
|
+
# =============================================================================
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class SlackMessage(BaseModel):
|
|
352
|
+
"""A single Slack message."""
|
|
353
|
+
|
|
354
|
+
message_id: str = Field(..., description="Slack message timestamp (ts)")
|
|
355
|
+
channel_id: str = Field(..., description="Channel ID where message was posted")
|
|
356
|
+
channel_name: str = Field(..., description="Channel name (human readable)")
|
|
357
|
+
user_id: str = Field(..., description="User ID who sent the message")
|
|
358
|
+
user_name: str = Field(..., description="User display name")
|
|
359
|
+
text: str = Field(..., description="Message text content")
|
|
360
|
+
timestamp: datetime = Field(..., description="When message was sent (UTC)")
|
|
361
|
+
thread_ts: str | None = Field(default=None, description="Parent thread timestamp if reply")
|
|
362
|
+
permalink: str | None = Field(default=None, description="Permalink to the message")
|
|
363
|
+
reactions: list[str] = Field(default_factory=list, description="Reaction emojis on message")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class SlackThread(BaseModel):
|
|
367
|
+
"""A Slack thread with parent message and replies."""
|
|
368
|
+
|
|
369
|
+
parent_message: SlackMessage = Field(..., description="The thread's parent message")
|
|
370
|
+
replies: list[SlackMessage] = Field(default_factory=list, description="Reply messages")
|
|
371
|
+
participant_names: list[str] = Field(default_factory=list, description="Unique participants")
|
|
372
|
+
decisions: list[str] = Field(default_factory=list, description="Decisions identified in thread")
|
|
373
|
+
action_items: list[str] = Field(default_factory=list, description="Action items from thread")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class SlackContext(BaseModel):
|
|
377
|
+
"""All Slack context found for a task."""
|
|
378
|
+
|
|
379
|
+
threads: list[SlackThread] = Field(default_factory=list, description="Relevant threads")
|
|
380
|
+
standalone_messages: list[SlackMessage] = Field(
|
|
381
|
+
default_factory=list,
|
|
382
|
+
description="Relevant messages not in threads",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# =============================================================================
|
|
387
|
+
# GMAIL DATA MODELS
|
|
388
|
+
# =============================================================================
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class GmailMessage(BaseModel):
|
|
392
|
+
"""A single Gmail message."""
|
|
393
|
+
|
|
394
|
+
message_id: str = Field(..., description="Gmail message ID")
|
|
395
|
+
thread_id: str = Field(..., description="Gmail thread/conversation ID")
|
|
396
|
+
subject: str = Field(..., description="Email subject line")
|
|
397
|
+
sender: str = Field(..., description="Sender email address")
|
|
398
|
+
sender_name: str | None = Field(default=None, description="Sender display name")
|
|
399
|
+
recipients: list[str] = Field(default_factory=list, description="To recipients")
|
|
400
|
+
cc: list[str] = Field(default_factory=list, description="CC recipients")
|
|
401
|
+
date: datetime = Field(..., description="When email was sent (UTC)")
|
|
402
|
+
snippet: str = Field(default="", description="Short preview of email body")
|
|
403
|
+
body_text: str = Field(default="", description="Plain text body content")
|
|
404
|
+
labels: list[str] = Field(default_factory=list, description="Gmail labels")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class GmailThread(BaseModel):
|
|
408
|
+
"""A Gmail conversation thread."""
|
|
409
|
+
|
|
410
|
+
thread_id: str = Field(..., description="Gmail thread ID")
|
|
411
|
+
subject: str = Field(..., description="Thread subject (from first message)")
|
|
412
|
+
messages: list[GmailMessage] = Field(default_factory=list, description="Messages in thread")
|
|
413
|
+
participants: list[str] = Field(default_factory=list, description="All participants")
|
|
414
|
+
latest_date: datetime = Field(..., description="Most recent message date")
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class GmailContext(BaseModel):
|
|
418
|
+
"""All Gmail context found for a task."""
|
|
419
|
+
|
|
420
|
+
threads: list[GmailThread] = Field(default_factory=list, description="Relevant email threads")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# =============================================================================
|
|
424
|
+
# DOCUMENTATION MODELS
|
|
425
|
+
# =============================================================================
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class DocSection(BaseModel):
|
|
429
|
+
"""A relevant section from a local document."""
|
|
430
|
+
|
|
431
|
+
file_path: str = Field(..., description="Path to the document file")
|
|
432
|
+
section_title: str | None = Field(default=None, description="Section title if identifiable")
|
|
433
|
+
content: str = Field(..., description="The section content")
|
|
434
|
+
doc_type: Literal["architecture", "standards", "adr", "other"] = Field(
|
|
435
|
+
default="other",
|
|
436
|
+
description="Type of documentation",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class DocsContext(BaseModel):
|
|
441
|
+
"""All relevant documentation found for a task."""
|
|
442
|
+
|
|
443
|
+
sections: list[DocSection] = Field(default_factory=list, description="Document sections")
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# =============================================================================
|
|
447
|
+
# RESULT MODELS
|
|
448
|
+
# =============================================================================
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class TaskContext(BaseModel):
|
|
452
|
+
"""The final synthesized context returned to the AI agent."""
|
|
453
|
+
|
|
454
|
+
task_id: str = Field(..., description="The task identifier that was queried")
|
|
455
|
+
synthesized: str = Field(..., description="LLM-generated structured markdown synthesis")
|
|
456
|
+
sources_used: list[str] = Field(default_factory=list, description="Sources that contributed")
|
|
457
|
+
fetch_duration_ms: int = Field(..., ge=0, description="How long the fetch took (ms)")
|
|
458
|
+
synthesized_at: datetime = Field(..., description="When context was synthesized (UTC)")
|
|
459
|
+
cached: bool = Field(default=False, description="Whether served from in-memory cache")
|
|
460
|
+
prebuilt: bool = Field(default=False, description="Whether served from pre-built storage")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class PrebuiltContext(BaseModel):
|
|
464
|
+
"""Pre-built context stored in SQLite for instant retrieval."""
|
|
465
|
+
|
|
466
|
+
task_id: str = Field(..., description="Task identifier (e.g., 'PROJ-123')")
|
|
467
|
+
synthesized: str = Field(..., description="Synthesized markdown context")
|
|
468
|
+
sources_used: list[str] = Field(default_factory=list, description="Sources that contributed")
|
|
469
|
+
context_quality_score: float = Field(
|
|
470
|
+
...,
|
|
471
|
+
ge=0.0,
|
|
472
|
+
le=1.0,
|
|
473
|
+
description="Quality score based on context completeness (0-1)",
|
|
474
|
+
)
|
|
475
|
+
gaps: list[str] = Field(
|
|
476
|
+
default_factory=list,
|
|
477
|
+
description="Identified gaps (e.g., 'No acceptance criteria')",
|
|
478
|
+
)
|
|
479
|
+
built_at: datetime = Field(..., description="When context was built (UTC)")
|
|
480
|
+
expires_at: datetime = Field(..., description="When context expires (UTC)")
|
|
481
|
+
source_data_hash: str = Field(
|
|
482
|
+
...,
|
|
483
|
+
description="Hash of Jira ticket.updated for staleness detection",
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
def is_expired(self) -> bool:
|
|
487
|
+
"""Check if this pre-built context has expired."""
|
|
488
|
+
return datetime.now(UTC) > self.expires_at
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class ContextData(BaseModel):
|
|
492
|
+
"""Structured context data from an adapter."""
|
|
493
|
+
|
|
494
|
+
source: str = Field(..., description="Source identifier (e.g., 'jira:PROJ-123')")
|
|
495
|
+
source_type: str = Field(..., description="Type of source (e.g., 'issue_tracker')")
|
|
496
|
+
title: str = Field(..., description="Human-readable title for this context item")
|
|
497
|
+
content: str = Field(..., description="The actual content/text of this context item")
|
|
498
|
+
metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
|
499
|
+
relevance_score: float = Field(
|
|
500
|
+
default=1.0,
|
|
501
|
+
ge=0.0,
|
|
502
|
+
le=1.0,
|
|
503
|
+
description="Relevance score from 0.0 to 1.0",
|
|
504
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Plugin system for DevsContext.
|
|
2
|
+
|
|
3
|
+
This package provides the plugin architecture for extensible sources and synthesis.
|
|
4
|
+
Third-party packages can implement Adapter or SynthesisPlugin to add new
|
|
5
|
+
data sources or synthesis strategies without modifying core code.
|
|
6
|
+
|
|
7
|
+
Plugin Interfaces:
|
|
8
|
+
- Adapter: Base class for data sources (Jira, Slack, Gmail, etc.)
|
|
9
|
+
- SynthesisPlugin: Base class for synthesis strategies (LLM, template, etc.)
|
|
10
|
+
|
|
11
|
+
Data Models:
|
|
12
|
+
- SourceContext: Container for data returned by an adapter
|
|
13
|
+
- SearchResult: A single search result from a source
|
|
14
|
+
|
|
15
|
+
Registry:
|
|
16
|
+
- PluginRegistry: Central registry for discovering and managing plugins
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
from devscontext.plugins import Adapter, SourceContext, PluginRegistry
|
|
20
|
+
|
|
21
|
+
class SlackAdapter(Adapter):
|
|
22
|
+
name = "slack"
|
|
23
|
+
source_type = "communication"
|
|
24
|
+
config_schema = SlackConfig
|
|
25
|
+
|
|
26
|
+
async def fetch_task_context(self, task_id, ticket=None):
|
|
27
|
+
# Fetch relevant Slack messages...
|
|
28
|
+
return SourceContext(...)
|
|
29
|
+
|
|
30
|
+
# Register the adapter
|
|
31
|
+
registry = PluginRegistry()
|
|
32
|
+
registry.register_adapter(SlackAdapter)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from devscontext.plugins.base import (
|
|
36
|
+
Adapter,
|
|
37
|
+
SearchResult,
|
|
38
|
+
SourceContext,
|
|
39
|
+
SynthesisPlugin,
|
|
40
|
+
)
|
|
41
|
+
from devscontext.plugins.registry import PluginRegistry
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"Adapter",
|
|
45
|
+
"PluginRegistry",
|
|
46
|
+
"SearchResult",
|
|
47
|
+
"SourceContext",
|
|
48
|
+
"SynthesisPlugin",
|
|
49
|
+
]
|