contextly 0.1.4__tar.gz → 0.1.7__tar.gz
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.
- {contextly-0.1.4 → contextly-0.1.7}/PKG-INFO +7 -12
- {contextly-0.1.4 → contextly-0.1.7}/pyproject.toml +8 -15
- contextly-0.1.7/requirements.txt +19 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/cli.py +49 -13
- contextly-0.1.7/src/contextly/core/__init__.py +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/core/analyzer.py +5 -5
- contextly-0.1.7/src/contextly/core/sync.py +89 -0
- contextly-0.1.7/src/contextly/llm/__init__.py +13 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/llm/manager.py +13 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/llm/models.py +24 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/llm/ollama.py +5 -0
- contextly-0.1.7/tests/conftest.py +33 -0
- contextly-0.1.7/tests/test_integration.py +170 -0
- contextly-0.1.4/src/contextly/core/sync.py +0 -66
- contextly-0.1.4/src/contextly/llm/__init__.py +0 -13
- {contextly-0.1.4 → contextly-0.1.7}/.gitignore +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/LICENSE +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/README.md +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/__init__.py +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/app.py +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/core/embeddings.py +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/llm/base.py +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/llm/openai.py +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/parsers/base.py +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/parsers/config.py +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/parsers/javascript.py +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/src/contextly/parsers/python.py +0 -0
- {contextly-0.1.4 → contextly-0.1.7}/tests/test_core.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextly
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: AI Context Engine for Developers
|
|
5
|
-
Project-URL: Homepage, https://github.com/
|
|
6
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
-
Project-URL: Documentation, https://github.com/
|
|
8
|
-
Project-URL: Bug Tracker, https://github.com/
|
|
9
|
-
Author-email: Contextly Team <
|
|
5
|
+
Project-URL: Homepage, https://github.com/desenyon/contextly
|
|
6
|
+
Project-URL: Repository, https://github.com/desenyon/contextly
|
|
7
|
+
Project-URL: Documentation, https://github.com/desenyon/contextly#readme
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/desenyon/contextly/issues
|
|
9
|
+
Author-email: Contextly Team <desenyon@gmail.com>
|
|
10
10
|
License-Expression: MIT
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: ai,analysis,code,context,development,documentation,llm
|
|
@@ -41,16 +41,11 @@ Requires-Dist: toml>=0.10.2
|
|
|
41
41
|
Requires-Dist: tqdm>=4.66.1
|
|
42
42
|
Requires-Dist: transformers>=4.35.0
|
|
43
43
|
Requires-Dist: typer>=0.9.0
|
|
44
|
-
Provides-Extra: dev
|
|
45
|
-
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
46
|
-
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
47
|
-
Requires-Dist: mypy>=1.5.0; extra == 'dev'
|
|
48
|
-
Requires-Dist: pre-commit>=3.3.0; extra == 'dev'
|
|
49
|
-
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
50
44
|
Provides-Extra: test
|
|
51
45
|
Requires-Dist: black>=23.0.0; extra == 'test'
|
|
52
46
|
Requires-Dist: isort>=5.12.0; extra == 'test'
|
|
53
47
|
Requires-Dist: mypy>=1.5.0; extra == 'test'
|
|
48
|
+
Requires-Dist: pre-commit>=3.3.0; extra == 'test'
|
|
54
49
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'test'
|
|
55
50
|
Requires-Dist: pytest-cov>=4.0.0; extra == 'test'
|
|
56
51
|
Requires-Dist: pytest>=7.0.0; extra == 'test'
|
|
@@ -4,9 +4,9 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "contextly"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7"
|
|
8
8
|
authors = [
|
|
9
|
-
{ name = "Contextly Team", email = "
|
|
9
|
+
{ name = "Contextly Team", email = "desenyon@gmail.com" },
|
|
10
10
|
]
|
|
11
11
|
description = "AI Context Engine for Developers"
|
|
12
12
|
readme = "README.md"
|
|
@@ -58,29 +58,22 @@ test = [
|
|
|
58
58
|
"isort>=5.12.0",
|
|
59
59
|
"mypy>=1.5.0",
|
|
60
60
|
"ruff>=0.1.0",
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
dev = [
|
|
64
|
-
"pre-commit>=3.3.0",
|
|
65
|
-
"black>=23.0.0",
|
|
66
|
-
"isort>=5.12.0",
|
|
67
|
-
"mypy>=1.5.0",
|
|
68
|
-
"ruff>=0.1.0",
|
|
61
|
+
"pre-commit>=3.3.0"
|
|
69
62
|
]
|
|
70
63
|
|
|
71
64
|
[project.scripts]
|
|
72
65
|
contextly = "contextly.cli:app"
|
|
73
66
|
|
|
74
67
|
[project.urls]
|
|
75
|
-
Homepage = "https://github.com/
|
|
76
|
-
Repository = "https://github.com/
|
|
77
|
-
Documentation = "https://github.com/
|
|
78
|
-
"Bug Tracker" = "https://github.com/
|
|
68
|
+
Homepage = "https://github.com/desenyon/contextly"
|
|
69
|
+
Repository = "https://github.com/desenyon/contextly"
|
|
70
|
+
Documentation = "https://github.com/desenyon/contextly#readme"
|
|
71
|
+
"Bug Tracker" = "https://github.com/desenyon/contextly/issues"
|
|
79
72
|
|
|
80
73
|
[tool.pytest.ini_options]
|
|
81
74
|
testpaths = ["tests"]
|
|
82
75
|
python_files = ["test_*.py"]
|
|
83
|
-
addopts = "
|
|
76
|
+
addopts = "-v"
|
|
84
77
|
|
|
85
78
|
[tool.black]
|
|
86
79
|
line-length = 100
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Core dependencies
|
|
2
|
+
pyyaml>=6.0.1
|
|
3
|
+
python-dotenv>=1.0.0
|
|
4
|
+
|
|
5
|
+
# LLM and embeddings
|
|
6
|
+
openai>=1.0.0
|
|
7
|
+
langchain>=0.0.300
|
|
8
|
+
transformers>=4.34.0
|
|
9
|
+
sentence-transformers>=2.2.2
|
|
10
|
+
|
|
11
|
+
# Testing
|
|
12
|
+
pytest>=7.4.2
|
|
13
|
+
pytest-cov>=4.1.0
|
|
14
|
+
|
|
15
|
+
# Development
|
|
16
|
+
black>=23.9.1
|
|
17
|
+
isort>=5.12.0
|
|
18
|
+
flake8>=6.1.0
|
|
19
|
+
mypy>=1.5.1
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Main CLI interface for Contextly
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import typer
|
|
6
7
|
from rich import print
|
|
7
8
|
from rich.console import Console
|
|
@@ -36,17 +37,28 @@ def ask(
|
|
|
36
37
|
print("🔍 Searching for answer...")
|
|
37
38
|
result = get_contextly().ask(question)
|
|
38
39
|
|
|
40
|
+
# Print answer or error in a panel
|
|
41
|
+
if 'error' in result:
|
|
42
|
+
console.print(Panel(
|
|
43
|
+
result['error'],
|
|
44
|
+
title="❌ Error",
|
|
45
|
+
border_style="red"
|
|
46
|
+
))
|
|
47
|
+
return
|
|
48
|
+
|
|
39
49
|
# Print answer in a panel
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
if 'answer' in result:
|
|
51
|
+
console.print(Panel(
|
|
52
|
+
Markdown(result['answer']),
|
|
53
|
+
title="💡 Answer",
|
|
54
|
+
border_style="blue"
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
# Print relevant code snippets
|
|
58
|
+
if result.get('context', {}).get('results'):
|
|
59
|
+
console.print("\n📚 Relevant code:")
|
|
60
|
+
for snippet in result['context']['results']:
|
|
61
|
+
console.print("") # Add a blank line for readability
|
|
50
62
|
console.print(Panel(
|
|
51
63
|
Syntax(
|
|
52
64
|
snippet['content'],
|
|
@@ -133,9 +145,33 @@ def sync(
|
|
|
133
145
|
),
|
|
134
146
|
) -> None:
|
|
135
147
|
"""Build or rebuild the local embedding index."""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
# Create fancy header
|
|
149
|
+
console.print(Panel(
|
|
150
|
+
"[bold blue]Contextly Repository Sync[/]",
|
|
151
|
+
subtitle="Building semantic search index",
|
|
152
|
+
style="blue"
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
contextly = get_contextly(path)
|
|
156
|
+
|
|
157
|
+
# Check model status first
|
|
158
|
+
model_name = os.getenv('CONTEXTLY_MODEL', 'codellama')
|
|
159
|
+
status = contextly.llm_manager.model_manager.registry.check_model_status(model_name)
|
|
160
|
+
|
|
161
|
+
if status['installed'] and status['ready']:
|
|
162
|
+
console.print(f"[green]✓[/] Using existing {model_name} model")
|
|
163
|
+
else:
|
|
164
|
+
console.print(f"[yellow]![/] {status['message']}")
|
|
165
|
+
console.print(f"[yellow]⚡[/] Downloading {model_name}...")
|
|
166
|
+
|
|
167
|
+
with console.status("[bold blue]📚 Analyzing repository structure...") as status:
|
|
168
|
+
try:
|
|
169
|
+
contextly.sync()
|
|
170
|
+
console.print("\n[green]✓[/] Repository indexed successfully!")
|
|
171
|
+
console.print("[dim]Run 'contextly ask' to start querying your codebase[/]")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
console.print(f"\n[red]✗[/] Error: {str(e)}")
|
|
174
|
+
raise typer.Exit(1)
|
|
139
175
|
|
|
140
176
|
@model_app.command("list")
|
|
141
177
|
def list_models() -> None:
|
|
File without changes
|
|
@@ -6,11 +6,11 @@ import difflib
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import List, Dict, Any, Optional
|
|
8
8
|
from typing import Type
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
from
|
|
9
|
+
from contextly.llm.manager import LLMManager
|
|
10
|
+
from contextly.parsers.base import BaseParser
|
|
11
|
+
from contextly.parsers.python import PythonParser
|
|
12
|
+
from contextly.parsers.javascript import JavaScriptParser
|
|
13
|
+
from contextly.parsers.config import ConfigParser
|
|
14
14
|
|
|
15
15
|
class Parser:
|
|
16
16
|
"""Base class for parsing different file types."""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repository synchronization and indexing functionality.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Dict, Any, Iterator, Tuple
|
|
7
|
+
import os
|
|
8
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
9
|
+
import multiprocessing
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
|
|
12
|
+
from ..parsers.python import PythonParser
|
|
13
|
+
from ..parsers.javascript import JavaScriptParser
|
|
14
|
+
from ..parsers.config import ConfigParser
|
|
15
|
+
|
|
16
|
+
class RepoSync:
|
|
17
|
+
"""Handles repository scanning and indexing."""
|
|
18
|
+
|
|
19
|
+
SUPPORTED_EXTENSIONS = {
|
|
20
|
+
# Code files
|
|
21
|
+
'.py': PythonParser,
|
|
22
|
+
'.js': JavaScriptParser,
|
|
23
|
+
'.jsx': JavaScriptParser,
|
|
24
|
+
'.ts': JavaScriptParser,
|
|
25
|
+
'.tsx': JavaScriptParser,
|
|
26
|
+
# Config files
|
|
27
|
+
'.json': ConfigParser,
|
|
28
|
+
'.yml': ConfigParser,
|
|
29
|
+
'.yaml': ConfigParser,
|
|
30
|
+
'.toml': ConfigParser,
|
|
31
|
+
'.env': ConfigParser,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
IGNORED_DIRS = {'.git', '__pycache__', 'node_modules', 'venv', '.venv', 'dist', 'build'}
|
|
35
|
+
|
|
36
|
+
def __init__(self, repo_path: Path):
|
|
37
|
+
self.repo_path = repo_path
|
|
38
|
+
self.num_workers = max(1, multiprocessing.cpu_count() - 1)
|
|
39
|
+
|
|
40
|
+
def scan_files(self) -> List[Path]:
|
|
41
|
+
"""Scan repository for supported files."""
|
|
42
|
+
files = []
|
|
43
|
+
for root, dirs, filenames in os.walk(self.repo_path):
|
|
44
|
+
# Skip ignored directories
|
|
45
|
+
dirs[:] = [d for d in dirs if d not in self.IGNORED_DIRS]
|
|
46
|
+
|
|
47
|
+
root_path = Path(root)
|
|
48
|
+
for file in filenames:
|
|
49
|
+
file_path = root_path / file
|
|
50
|
+
if file_path.suffix in self.SUPPORTED_EXTENSIONS:
|
|
51
|
+
files.append(file_path)
|
|
52
|
+
return files
|
|
53
|
+
|
|
54
|
+
def _process_file(self, file_path: Path) -> Tuple[str, Dict[str, Any]]:
|
|
55
|
+
"""Process a single file and return its index data."""
|
|
56
|
+
parser_class = self.SUPPORTED_EXTENSIONS.get(file_path.suffix)
|
|
57
|
+
if not parser_class:
|
|
58
|
+
return str(file_path), {}
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
parser = parser_class()
|
|
62
|
+
result = parser.parse(file_path)
|
|
63
|
+
# Add file path to chunks for reference
|
|
64
|
+
for chunk in result.get('chunks', []):
|
|
65
|
+
chunk['file_path'] = str(file_path)
|
|
66
|
+
return str(file_path), result
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return str(file_path), {'error': str(e)}
|
|
69
|
+
|
|
70
|
+
def index_repository(self) -> Dict[str, Any]:
|
|
71
|
+
"""Build index of repository contents using parallel processing."""
|
|
72
|
+
files = self.scan_files()
|
|
73
|
+
total_files = len(files)
|
|
74
|
+
|
|
75
|
+
if total_files == 0:
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
index = {}
|
|
79
|
+
with ProcessPoolExecutor(max_workers=self.num_workers) as executor:
|
|
80
|
+
futures = [executor.submit(self._process_file, f) for f in files]
|
|
81
|
+
|
|
82
|
+
with tqdm(total=total_files, desc="Analyzing files", unit="file") as pbar:
|
|
83
|
+
for future in as_completed(futures):
|
|
84
|
+
file_path, result = future.result()
|
|
85
|
+
if result and 'error' not in result:
|
|
86
|
+
index[file_path] = result
|
|
87
|
+
pbar.update(1)
|
|
88
|
+
|
|
89
|
+
return index
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""LLM package for Contextly."""
|
|
2
|
+
|
|
3
|
+
from contextly.llm.manager import LLMManager
|
|
4
|
+
from contextly.llm.base import LLMProvider
|
|
5
|
+
from contextly.llm.models import ModelManager, ModelRegistry, ModelProvider
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'LLMManager',
|
|
9
|
+
'LLMProvider',
|
|
10
|
+
'ModelManager',
|
|
11
|
+
'ModelRegistry',
|
|
12
|
+
'ModelProvider'
|
|
13
|
+
]
|
|
@@ -14,10 +14,13 @@ class LLMManager:
|
|
|
14
14
|
"""Manages LLM providers and generates code explanations."""
|
|
15
15
|
|
|
16
16
|
def __init__(self, model: Optional[str] = None):
|
|
17
|
+
"""Initialize the LLM manager with the given model."""
|
|
17
18
|
self.model_manager = ModelManager()
|
|
18
19
|
self.providers: Dict[str, LLMProvider] = {}
|
|
19
20
|
self.current_model = model or os.getenv('CONTEXTLY_MODEL', 'codellama')
|
|
21
|
+
self.initialized = False
|
|
20
22
|
self._initialize_providers()
|
|
23
|
+
self.initialized = True
|
|
21
24
|
|
|
22
25
|
def _initialize_providers(self) -> None:
|
|
23
26
|
"""Initialize LLM providers."""
|
|
@@ -27,6 +30,16 @@ class LLMManager:
|
|
|
27
30
|
# Add OpenAI if key is available
|
|
28
31
|
if os.getenv('OPENAI_API_KEY'):
|
|
29
32
|
self.providers['openai'] = OpenAIProvider()
|
|
33
|
+
|
|
34
|
+
# Ensure the default provider is available
|
|
35
|
+
self.ensure_default_provider()
|
|
36
|
+
|
|
37
|
+
def ensure_default_provider(self) -> None:
|
|
38
|
+
"""Ensure that a default provider is available."""
|
|
39
|
+
# Try to set up Ollama with codellama as default
|
|
40
|
+
if 'ollama' in self.providers:
|
|
41
|
+
if not self.model_manager.registry.get_model('codellama'):
|
|
42
|
+
self.model_manager.download_model('codellama', ModelProvider.OLLAMA)
|
|
30
43
|
|
|
31
44
|
def get_available_provider(self) -> Optional[LLMProvider]:
|
|
32
45
|
"""Get the appropriate provider for the current model."""
|
|
@@ -35,6 +35,30 @@ class ModelRegistry:
|
|
|
35
35
|
self.models: Dict[str, ModelInfo] = {}
|
|
36
36
|
self._load_models()
|
|
37
37
|
|
|
38
|
+
def check_model_status(self, model_name: str) -> Dict[str, Any]:
|
|
39
|
+
"""Check if a model is installed and ready to use."""
|
|
40
|
+
import subprocess
|
|
41
|
+
status = {
|
|
42
|
+
'installed': False,
|
|
43
|
+
'ready': False,
|
|
44
|
+
'message': ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if model_name.startswith('codellama'):
|
|
48
|
+
# Check if Ollama is running
|
|
49
|
+
try:
|
|
50
|
+
result = subprocess.run(['ollama', 'list'], capture_output=True, text=True)
|
|
51
|
+
models = result.stdout.strip().split('\n')
|
|
52
|
+
status['installed'] = any(model_name in model for model in models)
|
|
53
|
+
if status['installed']:
|
|
54
|
+
status['ready'] = True
|
|
55
|
+
status['message'] = '✅ Model is installed and ready'
|
|
56
|
+
else:
|
|
57
|
+
status['message'] = '⚠️ Model not found in Ollama'
|
|
58
|
+
except Exception:
|
|
59
|
+
status['message'] = '❌ Ollama not running or not installed'
|
|
60
|
+
return status
|
|
61
|
+
|
|
38
62
|
def _load_models(self) -> None:
|
|
39
63
|
"""Load model registry from config file."""
|
|
40
64
|
try:
|
|
@@ -12,6 +12,7 @@ class OllamaProvider(LLMProvider):
|
|
|
12
12
|
|
|
13
13
|
DEFAULT_MODEL = "codellama"
|
|
14
14
|
BASE_URL = "http://localhost:11434/api"
|
|
15
|
+
_model_checked = {} # Class variable to track which models have been checked
|
|
15
16
|
|
|
16
17
|
def __init__(self, model: str = DEFAULT_MODEL):
|
|
17
18
|
self.model = model
|
|
@@ -19,6 +20,9 @@ class OllamaProvider(LLMProvider):
|
|
|
19
20
|
|
|
20
21
|
def _ensure_model(self) -> None:
|
|
21
22
|
"""Ensure the model is downloaded and ready."""
|
|
23
|
+
if self.model in self._model_checked:
|
|
24
|
+
return
|
|
25
|
+
|
|
22
26
|
try:
|
|
23
27
|
# Check if model exists
|
|
24
28
|
response = requests.get(f"{self.BASE_URL}/tags")
|
|
@@ -34,6 +38,7 @@ class OllamaProvider(LLMProvider):
|
|
|
34
38
|
if pull_response.status_code != 200:
|
|
35
39
|
raise RuntimeError(f"Failed to pull model: {pull_response.text}")
|
|
36
40
|
print(f"{self.model} ready!")
|
|
41
|
+
self._model_checked[self.model] = True
|
|
37
42
|
except Exception as e:
|
|
38
43
|
raise RuntimeError(f"Failed to set up Ollama: {str(e)}")
|
|
39
44
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration and fixtures for pytest.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import pytest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def test_repo_path():
|
|
11
|
+
"""Get the path to the test repository."""
|
|
12
|
+
# Return the path to the contextly repository itself
|
|
13
|
+
return Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def setup_environment():
|
|
17
|
+
"""Set up test environment variables."""
|
|
18
|
+
# Store existing environment variables
|
|
19
|
+
old_env = {}
|
|
20
|
+
if 'CONTEXTLY_MODEL' in os.environ:
|
|
21
|
+
old_env['CONTEXTLY_MODEL'] = os.environ['CONTEXTLY_MODEL']
|
|
22
|
+
|
|
23
|
+
# Set test environment variables
|
|
24
|
+
os.environ['CONTEXTLY_MODEL'] = 'codellama'
|
|
25
|
+
|
|
26
|
+
yield
|
|
27
|
+
|
|
28
|
+
# Restore environment variables
|
|
29
|
+
for key in ['CONTEXTLY_MODEL']:
|
|
30
|
+
if key in old_env:
|
|
31
|
+
os.environ[key] = old_env[key]
|
|
32
|
+
elif key in os.environ:
|
|
33
|
+
del os.environ[key]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for Contextly using its own codebase as test data.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import pytest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from contextly.app import Contextly
|
|
9
|
+
from contextly.core.sync import RepoSync
|
|
10
|
+
from contextly.core.embeddings import EmbeddingEngine
|
|
11
|
+
from contextly.llm.manager import LLMManager
|
|
12
|
+
from contextly.llm.ollama import OllamaProvider
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def repo_path():
|
|
16
|
+
"""Get the path to the Contextly repository."""
|
|
17
|
+
# Get the tests directory and go up one level
|
|
18
|
+
return Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def contextly_app(repo_path):
|
|
22
|
+
"""Create a Contextly instance for testing."""
|
|
23
|
+
return Contextly(repo_path)
|
|
24
|
+
|
|
25
|
+
def test_repo_sync(repo_path):
|
|
26
|
+
"""Test repository synchronization with Contextly's own codebase."""
|
|
27
|
+
repo_sync = RepoSync(repo_path)
|
|
28
|
+
results = repo_sync.scan_files()
|
|
29
|
+
|
|
30
|
+
# Verify essential files are found
|
|
31
|
+
python_files = [f for f in results if str(f).endswith('.py')]
|
|
32
|
+
assert len(python_files) > 0, "Should find Python files"
|
|
33
|
+
|
|
34
|
+
# Verify core modules are found
|
|
35
|
+
core_files = {
|
|
36
|
+
'app.py',
|
|
37
|
+
'cli.py',
|
|
38
|
+
'embeddings.py',
|
|
39
|
+
'analyzer.py',
|
|
40
|
+
'sync.py'
|
|
41
|
+
}
|
|
42
|
+
found_files = {f.name for f in python_files}
|
|
43
|
+
assert core_files.intersection(found_files), "Should find core modules"
|
|
44
|
+
|
|
45
|
+
def test_embedding_engine(repo_path):
|
|
46
|
+
"""Test embedding engine with Contextly's codebase."""
|
|
47
|
+
engine = EmbeddingEngine(repo_path)
|
|
48
|
+
|
|
49
|
+
# Test search functionality
|
|
50
|
+
results = engine.search("What is the main purpose of this codebase?")
|
|
51
|
+
assert results is not None
|
|
52
|
+
assert 'results' in results
|
|
53
|
+
assert len(results['results']) > 0
|
|
54
|
+
|
|
55
|
+
# Verify search results contain relevant files
|
|
56
|
+
files = {r['file'] for r in results['results']}
|
|
57
|
+
assert any('README.md' in str(f) or 'app.py' in str(f) for f in files), \
|
|
58
|
+
"Should find relevant documentation or core files"
|
|
59
|
+
|
|
60
|
+
def test_llm_manager():
|
|
61
|
+
"""Test LLM manager functionality."""
|
|
62
|
+
manager = LLMManager(model='codellama')
|
|
63
|
+
assert manager.initialized
|
|
64
|
+
assert 'ollama' in manager.providers
|
|
65
|
+
|
|
66
|
+
# Test available provider
|
|
67
|
+
provider = manager.get_available_provider()
|
|
68
|
+
assert provider is not None
|
|
69
|
+
assert isinstance(provider, OllamaProvider)
|
|
70
|
+
|
|
71
|
+
# Test model is available
|
|
72
|
+
assert provider.is_available()
|
|
73
|
+
assert provider.model == 'codellama'
|
|
74
|
+
|
|
75
|
+
# Test code explanation with simple code
|
|
76
|
+
code = '''def greet(name: str) -> str:
|
|
77
|
+
"""Greet a user by name."""
|
|
78
|
+
return f"Hello, {name}!"'''
|
|
79
|
+
|
|
80
|
+
context = {
|
|
81
|
+
'question': 'What does this function do?',
|
|
82
|
+
'code_snippets': [code],
|
|
83
|
+
'files': ['test.py']
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
explanation = manager.explain_code(code, context)
|
|
87
|
+
assert explanation is not None
|
|
88
|
+
assert len(explanation) > 0
|
|
89
|
+
|
|
90
|
+
def test_ollama_provider():
|
|
91
|
+
"""Test Ollama LLM provider."""
|
|
92
|
+
provider = OllamaProvider()
|
|
93
|
+
|
|
94
|
+
# Test model availability
|
|
95
|
+
assert provider.is_available(), "Ollama should be running"
|
|
96
|
+
|
|
97
|
+
# Test response generation
|
|
98
|
+
prompt = "What is a code analyzer?"
|
|
99
|
+
response = provider.generate_response(prompt)
|
|
100
|
+
assert response is not None
|
|
101
|
+
assert len(response) > 0
|
|
102
|
+
|
|
103
|
+
def test_contextly_ask(contextly_app):
|
|
104
|
+
"""Test the ask functionality with real questions about the codebase."""
|
|
105
|
+
questions = [
|
|
106
|
+
"What is the purpose of the sync.py file?",
|
|
107
|
+
"How does the embedding engine work?",
|
|
108
|
+
"What LLM models are supported?",
|
|
109
|
+
"How are code snippets parsed?"
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
for question in questions:
|
|
113
|
+
result = contextly_app.ask(question)
|
|
114
|
+
assert result is not None
|
|
115
|
+
assert 'answer' in result or 'error' in result
|
|
116
|
+
|
|
117
|
+
if 'answer' in result:
|
|
118
|
+
assert len(result['answer']) > 0
|
|
119
|
+
# Verify context is provided when available
|
|
120
|
+
if result.get('context'):
|
|
121
|
+
assert 'results' in result['context']
|
|
122
|
+
assert len(result['context']['results']) > 0
|
|
123
|
+
|
|
124
|
+
def test_error_handling(contextly_app):
|
|
125
|
+
"""Test error handling in various scenarios."""
|
|
126
|
+
# Test with empty question
|
|
127
|
+
with pytest.raises(Exception):
|
|
128
|
+
contextly_app.ask("")
|
|
129
|
+
|
|
130
|
+
# Test with very long question
|
|
131
|
+
long_question = "what " * 1000
|
|
132
|
+
result = contextly_app.ask(long_question)
|
|
133
|
+
assert 'error' in result or 'answer' in result
|
|
134
|
+
|
|
135
|
+
# Test with invalid repository path
|
|
136
|
+
invalid_app = Contextly(Path("/nonexistent/path"))
|
|
137
|
+
result = invalid_app.ask("What is this?")
|
|
138
|
+
assert 'error' in result
|
|
139
|
+
|
|
140
|
+
def test_code_analysis(contextly_app):
|
|
141
|
+
"""Test code analysis functionality."""
|
|
142
|
+
# Get insights about a specific file
|
|
143
|
+
core_files = [
|
|
144
|
+
'app.py',
|
|
145
|
+
'cli.py',
|
|
146
|
+
'embeddings.py'
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for file in core_files:
|
|
150
|
+
question = f"What does {file} do?"
|
|
151
|
+
result = contextly_app.ask(question)
|
|
152
|
+
assert result is not None
|
|
153
|
+
assert 'answer' in result
|
|
154
|
+
assert len(result['answer']) > 0
|
|
155
|
+
|
|
156
|
+
def test_multi_file_context(contextly_app):
|
|
157
|
+
"""Test handling questions that require context from multiple files."""
|
|
158
|
+
questions = [
|
|
159
|
+
"How does the CLI interface interact with the core application?",
|
|
160
|
+
"What is the relationship between embeddings.py and analyzer.py?",
|
|
161
|
+
"How do the different LLM providers work together?"
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
for question in questions:
|
|
165
|
+
result = contextly_app.ask(question)
|
|
166
|
+
assert result is not None
|
|
167
|
+
assert 'answer' in result
|
|
168
|
+
if result.get('context'):
|
|
169
|
+
files = {r['file'] for r in result['context']['results']}
|
|
170
|
+
assert len(files) > 1, "Should reference multiple files for complex questions"
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Repository synchronization and indexing functionality.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import List, Dict, Any, Iterator
|
|
7
|
-
import os
|
|
8
|
-
|
|
9
|
-
from ..parsers.python import PythonParser
|
|
10
|
-
from ..parsers.javascript import JavaScriptParser
|
|
11
|
-
from ..parsers.config import ConfigParser
|
|
12
|
-
|
|
13
|
-
class RepoSync:
|
|
14
|
-
"""Handles repository scanning and indexing."""
|
|
15
|
-
|
|
16
|
-
SUPPORTED_EXTENSIONS = {
|
|
17
|
-
'.py', '.js', '.json', '.yml', '.yaml',
|
|
18
|
-
'.env', '.toml', '.md', '.txt'
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
def __init__(self, repo_path: Path):
|
|
22
|
-
self.repo_path = repo_path
|
|
23
|
-
|
|
24
|
-
def scan_files(self) -> Iterator[Path]:
|
|
25
|
-
"""Scan repository for supported files."""
|
|
26
|
-
for root, _, files in os.walk(self.repo_path):
|
|
27
|
-
root_path = Path(root)
|
|
28
|
-
if '.git' in root_path.parts:
|
|
29
|
-
continue
|
|
30
|
-
|
|
31
|
-
for file in files:
|
|
32
|
-
file_path = root_path / file
|
|
33
|
-
if file_path.suffix in self.SUPPORTED_EXTENSIONS:
|
|
34
|
-
yield file_path
|
|
35
|
-
|
|
36
|
-
def index_repository(self) -> Dict[str, Any]:
|
|
37
|
-
"""Build index of repository contents."""
|
|
38
|
-
index = {}
|
|
39
|
-
parsers = {
|
|
40
|
-
'.py': PythonParser(),
|
|
41
|
-
'.js': JavaScriptParser(),
|
|
42
|
-
'.jsx': JavaScriptParser(),
|
|
43
|
-
'.ts': JavaScriptParser(),
|
|
44
|
-
'.tsx': JavaScriptParser(),
|
|
45
|
-
'.json': ConfigParser(),
|
|
46
|
-
'.yml': ConfigParser(),
|
|
47
|
-
'.yaml': ConfigParser(),
|
|
48
|
-
'.toml': ConfigParser(),
|
|
49
|
-
'.env': ConfigParser(),
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
for file_path in self.scan_files():
|
|
53
|
-
ext = file_path.suffix
|
|
54
|
-
parser = parsers.get(ext)
|
|
55
|
-
|
|
56
|
-
if parser:
|
|
57
|
-
try:
|
|
58
|
-
result = parser.parse(file_path)
|
|
59
|
-
# Add file path to chunks for reference
|
|
60
|
-
for chunk in result.get('chunks', []):
|
|
61
|
-
chunk['file_path'] = str(file_path)
|
|
62
|
-
index[str(file_path)] = result
|
|
63
|
-
except Exception as e:
|
|
64
|
-
print(f"Error parsing {file_path}: {e}")
|
|
65
|
-
|
|
66
|
-
return index
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"""LLM package for Contextly."""
|
|
2
|
-
|
|
3
|
-
from .manager import LLMManager
|
|
4
|
-
from .base import LLMProvider
|
|
5
|
-
from .models import ModelManager, ModelRegistry, ModelProvider
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
'LLMManager',
|
|
9
|
-
'LLMProvider',
|
|
10
|
-
'ModelManager',
|
|
11
|
-
'ModelRegistry',
|
|
12
|
-
'ModelProvider'
|
|
13
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|