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/config.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Configuration loader for DevsContext.
|
|
2
|
+
|
|
3
|
+
This module handles loading and parsing configuration from YAML files,
|
|
4
|
+
with support for environment variable expansion.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
config = load_devscontext_config()
|
|
8
|
+
if config.sources.jira.enabled:
|
|
9
|
+
print(f"Jira URL: {config.sources.jira.base_url}")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
|
|
22
|
+
from devscontext.constants import (
|
|
23
|
+
CONFIG_FILE_NAME,
|
|
24
|
+
DEFAULT_CACHE_MAX_SIZE,
|
|
25
|
+
DEFAULT_CACHE_TTL_SECONDS,
|
|
26
|
+
)
|
|
27
|
+
from devscontext.models import DevsContextConfig
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class JiraConfig(BaseModel):
|
|
31
|
+
"""Jira adapter configuration.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
base_url: The Jira instance URL (e.g., https://company.atlassian.net).
|
|
35
|
+
email: Email address for Jira authentication.
|
|
36
|
+
api_token: API token for Jira authentication.
|
|
37
|
+
enabled: Whether the Jira adapter is enabled.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
base_url: str = Field(default="", description="Jira instance URL")
|
|
41
|
+
email: str = Field(default="", description="Jira authentication email")
|
|
42
|
+
api_token: str = Field(default="", description="Jira API token")
|
|
43
|
+
enabled: bool = Field(default=False, description="Whether adapter is enabled")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FirefliesConfig(BaseModel):
|
|
47
|
+
"""Fireflies.ai adapter configuration.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
api_key: API key for Fireflies.ai authentication.
|
|
51
|
+
enabled: Whether the Fireflies adapter is enabled.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
api_key: str = Field(default="", description="Fireflies.ai API key")
|
|
55
|
+
enabled: bool = Field(default=False, description="Whether adapter is enabled")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class LocalDocsConfig(BaseModel):
|
|
59
|
+
"""Local documentation adapter configuration.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
paths: List of directory paths to search for documentation.
|
|
63
|
+
enabled: Whether the local docs adapter is enabled.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
paths: list[str] = Field(default_factory=list, description="Paths to doc directories")
|
|
67
|
+
enabled: bool = Field(default=True, description="Whether adapter is enabled")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AdaptersConfig(BaseModel):
|
|
71
|
+
"""Configuration for all adapters.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
jira: Jira adapter configuration.
|
|
75
|
+
fireflies: Fireflies adapter configuration.
|
|
76
|
+
local_docs: Local docs adapter configuration.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
jira: JiraConfig = Field(default_factory=JiraConfig)
|
|
80
|
+
fireflies: FirefliesConfig = Field(default_factory=FirefliesConfig)
|
|
81
|
+
local_docs: LocalDocsConfig = Field(default_factory=LocalDocsConfig)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CacheConfig(BaseModel):
|
|
85
|
+
"""Cache configuration.
|
|
86
|
+
|
|
87
|
+
Attributes:
|
|
88
|
+
ttl_seconds: Time-to-live in seconds for cache entries.
|
|
89
|
+
max_size: Maximum number of entries in the cache.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
ttl_seconds: int = Field(
|
|
93
|
+
default=DEFAULT_CACHE_TTL_SECONDS,
|
|
94
|
+
description="Cache entry TTL in seconds",
|
|
95
|
+
)
|
|
96
|
+
max_size: int = Field(
|
|
97
|
+
default=DEFAULT_CACHE_MAX_SIZE,
|
|
98
|
+
description="Maximum cache entries",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Config(BaseModel):
|
|
103
|
+
"""Root configuration for DevsContext.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
adapters: Configuration for all adapters.
|
|
107
|
+
cache: Cache configuration.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
adapters: AdaptersConfig = Field(default_factory=AdaptersConfig)
|
|
111
|
+
cache: CacheConfig = Field(default_factory=CacheConfig)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Pattern to match ${VAR_NAME} or $VAR_NAME
|
|
115
|
+
ENV_VAR_PATTERN = re.compile(r"\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def expand_env_vars(value: Any) -> Any:
|
|
119
|
+
"""Recursively expand environment variables in config values.
|
|
120
|
+
|
|
121
|
+
Supports both ${VAR_NAME} and $VAR_NAME syntax.
|
|
122
|
+
If the env var is not set, the placeholder is left unchanged.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
value: The value to expand (can be str, dict, list, or primitive).
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The value with environment variables expanded.
|
|
129
|
+
"""
|
|
130
|
+
if isinstance(value, str):
|
|
131
|
+
|
|
132
|
+
def replace_env_var(match: re.Match[str]) -> str:
|
|
133
|
+
# Group 1 is ${VAR}, group 2 is $VAR
|
|
134
|
+
var_name = match.group(1) or match.group(2)
|
|
135
|
+
return os.environ.get(var_name, match.group(0))
|
|
136
|
+
|
|
137
|
+
return ENV_VAR_PATTERN.sub(replace_env_var, value)
|
|
138
|
+
|
|
139
|
+
elif isinstance(value, dict):
|
|
140
|
+
return {k: expand_env_vars(v) for k, v in value.items()}
|
|
141
|
+
|
|
142
|
+
elif isinstance(value, list):
|
|
143
|
+
return [expand_env_vars(item) for item in value]
|
|
144
|
+
|
|
145
|
+
else:
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def load_config(config_path: Path | None = None) -> Config:
|
|
150
|
+
"""Load configuration from YAML file.
|
|
151
|
+
|
|
152
|
+
Environment variables in the format ${VAR_NAME} or $VAR_NAME are expanded.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
config_path: Path to config file. If None, searches for .devscontext.yaml
|
|
156
|
+
in current directory and parent directories.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Loaded configuration with env vars expanded.
|
|
160
|
+
"""
|
|
161
|
+
if config_path is None:
|
|
162
|
+
config_path = find_config_file()
|
|
163
|
+
|
|
164
|
+
if config_path is None or not config_path.exists():
|
|
165
|
+
return Config()
|
|
166
|
+
|
|
167
|
+
with open(config_path) as f:
|
|
168
|
+
data: dict[str, Any] = yaml.safe_load(f) or {}
|
|
169
|
+
|
|
170
|
+
# Expand environment variables
|
|
171
|
+
data = expand_env_vars(data)
|
|
172
|
+
|
|
173
|
+
return Config.model_validate(data)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def find_config_file() -> Path | None:
|
|
177
|
+
"""Search for .devscontext.yaml in current and parent directories.
|
|
178
|
+
|
|
179
|
+
Walks up the directory tree from the current working directory,
|
|
180
|
+
looking for a configuration file.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Path to the config file if found, None otherwise.
|
|
184
|
+
"""
|
|
185
|
+
current = Path.cwd()
|
|
186
|
+
|
|
187
|
+
for directory in [current, *current.parents]:
|
|
188
|
+
config_file = directory / CONFIG_FILE_NAME
|
|
189
|
+
if config_file.exists():
|
|
190
|
+
return config_file
|
|
191
|
+
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def load_devscontext_config(config_path: Path | None = None) -> DevsContextConfig:
|
|
196
|
+
"""Load DevsContextConfig from YAML file.
|
|
197
|
+
|
|
198
|
+
This is the new config loader that returns DevsContextConfig with
|
|
199
|
+
the sources/synthesis/cache structure.
|
|
200
|
+
|
|
201
|
+
Environment variables in the format ${VAR_NAME} or $VAR_NAME are expanded.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
config_path: Path to config file. If None, searches for .devscontext.yaml
|
|
205
|
+
in current directory and parent directories.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Loaded DevsContextConfig with env vars expanded.
|
|
209
|
+
"""
|
|
210
|
+
if config_path is None:
|
|
211
|
+
config_path = find_config_file()
|
|
212
|
+
|
|
213
|
+
if config_path is None or not config_path.exists():
|
|
214
|
+
return DevsContextConfig()
|
|
215
|
+
|
|
216
|
+
with open(config_path) as f:
|
|
217
|
+
data: dict[str, Any] = yaml.safe_load(f) or {}
|
|
218
|
+
|
|
219
|
+
# Expand environment variables
|
|
220
|
+
data = expand_env_vars(data)
|
|
221
|
+
|
|
222
|
+
# Transform old config format to new format if needed
|
|
223
|
+
if "adapters" in data and "sources" not in data:
|
|
224
|
+
data = _transform_legacy_config(data)
|
|
225
|
+
|
|
226
|
+
return DevsContextConfig.model_validate(data)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _transform_legacy_config(data: dict[str, Any]) -> dict[str, Any]:
|
|
230
|
+
"""Transform legacy config format to new format.
|
|
231
|
+
|
|
232
|
+
Legacy format uses 'adapters' key with 'local_docs'.
|
|
233
|
+
New format uses 'sources' key with 'docs'.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
data: Legacy config data.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Transformed config data for DevsContextConfig.
|
|
240
|
+
"""
|
|
241
|
+
adapters = data.pop("adapters", {})
|
|
242
|
+
cache = data.get("cache", {})
|
|
243
|
+
|
|
244
|
+
# Transform adapters to sources
|
|
245
|
+
sources: dict[str, Any] = {}
|
|
246
|
+
|
|
247
|
+
if "jira" in adapters:
|
|
248
|
+
sources["jira"] = adapters["jira"]
|
|
249
|
+
|
|
250
|
+
if "fireflies" in adapters:
|
|
251
|
+
sources["fireflies"] = adapters["fireflies"]
|
|
252
|
+
|
|
253
|
+
if "local_docs" in adapters:
|
|
254
|
+
sources["docs"] = adapters["local_docs"]
|
|
255
|
+
|
|
256
|
+
# Convert cache TTL from seconds to minutes if present
|
|
257
|
+
if "ttl_seconds" in cache and "ttl_minutes" not in cache:
|
|
258
|
+
cache["ttl_minutes"] = cache.pop("ttl_seconds") // 60
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
"sources": sources,
|
|
262
|
+
"synthesis": data.get("synthesis", {}),
|
|
263
|
+
"cache": cache,
|
|
264
|
+
}
|
devscontext/constants.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Constants and configuration defaults for DevsContext.
|
|
2
|
+
|
|
3
|
+
This module contains all magic values, default configurations, and constants
|
|
4
|
+
used throughout the application. Import from here instead of hardcoding values.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
# =============================================================================
|
|
10
|
+
# VERSION
|
|
11
|
+
# =============================================================================
|
|
12
|
+
VERSION: Final[str] = "0.1.0"
|
|
13
|
+
|
|
14
|
+
# =============================================================================
|
|
15
|
+
# CACHE DEFAULTS
|
|
16
|
+
# =============================================================================
|
|
17
|
+
DEFAULT_CACHE_TTL_SECONDS: Final[int] = 900 # 15 minutes
|
|
18
|
+
DEFAULT_CACHE_MAX_SIZE: Final[int] = 100
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# HTTP CLIENT DEFAULTS
|
|
22
|
+
# =============================================================================
|
|
23
|
+
DEFAULT_HTTP_TIMEOUT_SECONDS: Final[float] = 30.0
|
|
24
|
+
DEFAULT_HTTP_MAX_RETRIES: Final[int] = 3
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# JIRA API
|
|
28
|
+
# =============================================================================
|
|
29
|
+
JIRA_API_VERSION: Final[str] = "3"
|
|
30
|
+
JIRA_API_BASE_PATH: Final[str] = f"/rest/api/{JIRA_API_VERSION}"
|
|
31
|
+
JIRA_MAX_COMMENTS: Final[int] = 50
|
|
32
|
+
JIRA_TICKET_FIELDS: Final[str] = (
|
|
33
|
+
"summary,description,status,priority,assignee,reporter,labels,issuetype,created,updated"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# =============================================================================
|
|
37
|
+
# FIREFLIES API
|
|
38
|
+
# =============================================================================
|
|
39
|
+
FIREFLIES_API_URL: Final[str] = "https://api.fireflies.ai/graphql"
|
|
40
|
+
FIREFLIES_MAX_TRANSCRIPTS: Final[int] = 10
|
|
41
|
+
FIREFLIES_SEARCH_LIMIT: Final[int] = 5
|
|
42
|
+
FIREFLIES_CONTEXT_WINDOW: Final[int] = 3 # Sentences before/after a match to include
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# LOCAL DOCS
|
|
46
|
+
# =============================================================================
|
|
47
|
+
SUPPORTED_DOC_EXTENSIONS: Final[tuple[str, ...]] = (".md", ".markdown", ".txt", ".rst")
|
|
48
|
+
MAX_DOC_FILE_SIZE_BYTES: Final[int] = 1_000_000 # 1MB
|
|
49
|
+
MAX_DOCS_TO_SEARCH: Final[int] = 100
|
|
50
|
+
|
|
51
|
+
# =============================================================================
|
|
52
|
+
# SYNTHESIS / LLM
|
|
53
|
+
# =============================================================================
|
|
54
|
+
DEFAULT_LLM_MODEL: Final[str] = "claude-3-haiku-20240307"
|
|
55
|
+
MAX_CONTEXT_LENGTH_CHARS: Final[int] = 100_000
|
|
56
|
+
MAX_SYNTHESIS_INPUT_CHARS: Final[int] = 50_000
|
|
57
|
+
|
|
58
|
+
# =============================================================================
|
|
59
|
+
# MCP SERVER
|
|
60
|
+
# =============================================================================
|
|
61
|
+
MCP_SERVER_NAME: Final[str] = "devscontext"
|
|
62
|
+
|
|
63
|
+
# =============================================================================
|
|
64
|
+
# CONFIG FILE
|
|
65
|
+
# =============================================================================
|
|
66
|
+
CONFIG_FILE_NAME: Final[str] = ".devscontext.yaml"
|
|
67
|
+
CONFIG_EXAMPLE_FILE_NAME: Final[str] = ".devscontext.yaml.example"
|
|
68
|
+
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# LOGGING
|
|
71
|
+
# =============================================================================
|
|
72
|
+
LOG_FORMAT: Final[str] = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
|
73
|
+
LOG_DATE_FORMAT: Final[str] = "%Y-%m-%d %H:%M:%S"
|
|
74
|
+
|
|
75
|
+
# =============================================================================
|
|
76
|
+
# ADAPTER NAMES (for consistent referencing)
|
|
77
|
+
# =============================================================================
|
|
78
|
+
ADAPTER_JIRA: Final[str] = "jira"
|
|
79
|
+
ADAPTER_FIREFLIES: Final[str] = "fireflies"
|
|
80
|
+
ADAPTER_LOCAL_DOCS: Final[str] = "local_docs"
|
|
81
|
+
ADAPTER_SLACK: Final[str] = "slack"
|
|
82
|
+
ADAPTER_GMAIL: Final[str] = "gmail"
|
|
83
|
+
|
|
84
|
+
# =============================================================================
|
|
85
|
+
# SOURCE TYPES
|
|
86
|
+
# =============================================================================
|
|
87
|
+
SOURCE_TYPE_ISSUE_TRACKER: Final[str] = "issue_tracker"
|
|
88
|
+
SOURCE_TYPE_MEETING: Final[str] = "meeting"
|
|
89
|
+
SOURCE_TYPE_DOCUMENTATION: Final[str] = "documentation"
|
|
90
|
+
SOURCE_TYPE_COMMUNICATION: Final[str] = "communication"
|
|
91
|
+
SOURCE_TYPE_EMAIL: Final[str] = "email"
|
|
92
|
+
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# SLACK API
|
|
95
|
+
# =============================================================================
|
|
96
|
+
SLACK_API_BASE_URL: Final[str] = "https://slack.com/api"
|
|
97
|
+
SLACK_RATE_LIMIT_REQUESTS_PER_MINUTE: Final[int] = 50
|
|
98
|
+
SLACK_CHANNEL_HISTORY_CACHE_TTL: Final[int] = 300 # 5 minutes
|
|
99
|
+
SLACK_MAX_MESSAGES_PER_CHANNEL: Final[int] = 100
|
|
100
|
+
SLACK_THREAD_REPLY_LIMIT: Final[int] = 50
|
|
101
|
+
|
|
102
|
+
# =============================================================================
|
|
103
|
+
# GMAIL API
|
|
104
|
+
# =============================================================================
|
|
105
|
+
GMAIL_API_SCOPES: Final[tuple[str, ...]] = ("https://www.googleapis.com/auth/gmail.readonly",)
|
|
106
|
+
GMAIL_BODY_MAX_CHARS: Final[int] = 5000
|
|
107
|
+
GMAIL_MAX_RESULTS_PER_QUERY: Final[int] = 25
|