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.
Files changed (28) hide show
  1. {contextly-0.1.4 → contextly-0.1.7}/PKG-INFO +7 -12
  2. {contextly-0.1.4 → contextly-0.1.7}/pyproject.toml +8 -15
  3. contextly-0.1.7/requirements.txt +19 -0
  4. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/cli.py +49 -13
  5. contextly-0.1.7/src/contextly/core/__init__.py +0 -0
  6. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/core/analyzer.py +5 -5
  7. contextly-0.1.7/src/contextly/core/sync.py +89 -0
  8. contextly-0.1.7/src/contextly/llm/__init__.py +13 -0
  9. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/llm/manager.py +13 -0
  10. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/llm/models.py +24 -0
  11. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/llm/ollama.py +5 -0
  12. contextly-0.1.7/tests/conftest.py +33 -0
  13. contextly-0.1.7/tests/test_integration.py +170 -0
  14. contextly-0.1.4/src/contextly/core/sync.py +0 -66
  15. contextly-0.1.4/src/contextly/llm/__init__.py +0 -13
  16. {contextly-0.1.4 → contextly-0.1.7}/.gitignore +0 -0
  17. {contextly-0.1.4 → contextly-0.1.7}/LICENSE +0 -0
  18. {contextly-0.1.4 → contextly-0.1.7}/README.md +0 -0
  19. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/__init__.py +0 -0
  20. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/app.py +0 -0
  21. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/core/embeddings.py +0 -0
  22. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/llm/base.py +0 -0
  23. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/llm/openai.py +0 -0
  24. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/parsers/base.py +0 -0
  25. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/parsers/config.py +0 -0
  26. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/parsers/javascript.py +0 -0
  27. {contextly-0.1.4 → contextly-0.1.7}/src/contextly/parsers/python.py +0 -0
  28. {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.4
3
+ Version: 0.1.7
4
4
  Summary: AI Context Engine for Developers
5
- Project-URL: Homepage, https://github.com/contextly/contextly
6
- Project-URL: Repository, https://github.com/contextly/contextly
7
- Project-URL: Documentation, https://github.com/contextly/contextly#readme
8
- Project-URL: Bug Tracker, https://github.com/contextly/contextly/issues
9
- Author-email: Contextly Team <team@contextly.dev>
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.4"
7
+ version = "0.1.7"
8
8
  authors = [
9
- { name = "Contextly Team", email = "team@contextly.dev" },
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/contextly/contextly"
76
- Repository = "https://github.com/contextly/contextly"
77
- Documentation = "https://github.com/contextly/contextly#readme"
78
- "Bug Tracker" = "https://github.com/contextly/contextly/issues"
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 = "--cov=contextly --cov-report=term-missing"
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
- console.print(Panel(
41
- Markdown(result['answer']),
42
- title="💡 Answer",
43
- border_style="blue"
44
- ))
45
-
46
- # Print relevant code snippets
47
- if result.get('context', {}).get('results'):
48
- console.print("\n📚 Relevant code:")
49
- for snippet in result['context']['results']:
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
- print("🔄 Syncing repository...")
137
- get_contextly(path).sync()
138
- print(" Repository indexed successfully!")
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 ..llm import LLMManager
10
- from ..parsers.base import BaseParser
11
- from ..parsers.python import PythonParser
12
- from ..parsers.javascript import JavaScriptParser
13
- from ..parsers.config import ConfigParser
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