Skip to content

Commit 4057109

Browse files
CopilotMte90
andcommitted
Add service layer for better code organization
Co-authored-by: Mte90 <403283+Mte90@users.noreply.github.com>
1 parent a9d3115 commit 4057109

File tree

4 files changed

+320
-20
lines changed

4 files changed

+320
-20
lines changed

main.py

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from datetime import datetime
1111

1212
from db import init_db, get_project_stats
13-
from analyzer import analyze_local_path_background, search_semantic, call_coding_model
13+
from analyzer import analyze_local_path_background, call_coding_model
1414
from config import CFG
1515
from projects import (
1616
get_project_by_id, list_projects,
@@ -22,6 +22,7 @@
2222
)
2323
from logger import get_logger
2424
from rate_limiter import query_limiter, indexing_limiter, general_limiter
25+
from services import ProjectService, SearchService
2526

2627
logger = get_logger(__name__)
2728

@@ -259,25 +260,18 @@ def api_query(http_request: Request, request: QueryRequest):
259260
)
260261

261262
try:
262-
project = get_project_by_id(request.project_id)
263-
if not project:
264-
return JSONResponse({"error": "Project not found"}, status_code=404)
265-
266-
db_path = project["database_path"]
267-
268-
# Check if project has been indexed
269-
stats = get_project_stats(db_path)
270-
if stats["file_count"] == 0:
271-
return JSONResponse({"error": "Project not indexed yet"}, status_code=400)
272-
273-
# Perform semantic search
274-
results = search_semantic(request.query, db_path, top_k=request.top_k)
275-
276-
return JSONResponse({
277-
"results": results,
278-
"project_id": request.project_id,
279-
"query": request.query
280-
})
263+
# Use SearchService for better separation of concerns
264+
result = SearchService.semantic_search(
265+
project_id=request.project_id,
266+
query=request.query,
267+
top_k=request.top_k,
268+
use_cache=True
269+
)
270+
return JSONResponse(result)
271+
except ValueError as e:
272+
# ValueError for not found or not indexed
273+
logger.warning(f"Query validation failed: {e}")
274+
return JSONResponse({"error": str(e)}, status_code=400)
281275
except Exception as e:
282276
logger.exception(f"Error querying project: {e}")
283277
return JSONResponse({"error": "Query failed"}, status_code=500)

services/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
Service layer initialization.
3+
Provides high-level business logic separated from database operations.
4+
"""
5+
from .project_service import ProjectService
6+
from .search_service import SearchService
7+
8+
__all__ = [
9+
'ProjectService',
10+
'SearchService',
11+
]

services/project_service.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""
2+
Service layer for project operations.
3+
Separates business logic from database operations.
4+
"""
5+
import os
6+
from typing import Dict, Any, Optional
7+
from datetime import datetime
8+
9+
from db import (
10+
create_project as db_create_project,
11+
get_project as db_get_project,
12+
get_project_by_id as db_get_project_by_id,
13+
list_projects as db_list_projects,
14+
update_project_status as db_update_project_status,
15+
delete_project as db_delete_project,
16+
get_or_create_project as db_get_or_create_project,
17+
get_project_stats,
18+
)
19+
from logger import get_logger
20+
21+
logger = get_logger(__name__)
22+
23+
24+
class ProjectService:
25+
"""
26+
Service layer for project management operations.
27+
Provides high-level business logic for projects.
28+
"""
29+
30+
@staticmethod
31+
def create_project(project_path: str, name: Optional[str] = None) -> Dict[str, Any]:
32+
"""
33+
Create a new project with validation.
34+
35+
Args:
36+
project_path: Path to project directory
37+
name: Optional project name
38+
39+
Returns:
40+
Project metadata dictionary
41+
42+
Raises:
43+
ValueError: If path is invalid
44+
RuntimeError: If creation fails
45+
"""
46+
# Validate path
47+
if not project_path:
48+
raise ValueError("Project path cannot be empty")
49+
50+
abs_path = os.path.abspath(project_path)
51+
52+
if not os.path.exists(abs_path):
53+
raise ValueError(f"Project path does not exist: {abs_path}")
54+
55+
if not os.path.isdir(abs_path):
56+
raise ValueError(f"Project path is not a directory: {abs_path}")
57+
58+
# Create project
59+
try:
60+
project = db_create_project(abs_path, name)
61+
logger.info(f"Created project {project['id']} at {abs_path}")
62+
return project
63+
except Exception as e:
64+
logger.error(f"Failed to create project: {e}")
65+
raise RuntimeError(f"Failed to create project: {e}") from e
66+
67+
@staticmethod
68+
def get_project(project_path: str) -> Optional[Dict[str, Any]]:
69+
"""Get project by path."""
70+
return db_get_project(project_path)
71+
72+
@staticmethod
73+
def get_project_by_id(project_id: str) -> Optional[Dict[str, Any]]:
74+
"""Get project by ID."""
75+
return db_get_project_by_id(project_id)
76+
77+
@staticmethod
78+
def list_all_projects() -> list:
79+
"""List all projects."""
80+
return db_list_projects()
81+
82+
@staticmethod
83+
def delete_project(project_id: str) -> None:
84+
"""
85+
Delete a project with validation.
86+
87+
Args:
88+
project_id: Project identifier
89+
90+
Raises:
91+
ValueError: If project not found
92+
"""
93+
project = db_get_project_by_id(project_id)
94+
if not project:
95+
raise ValueError(f"Project not found: {project_id}")
96+
97+
try:
98+
db_delete_project(project_id)
99+
logger.info(f"Deleted project {project_id}")
100+
except Exception as e:
101+
logger.error(f"Failed to delete project: {e}")
102+
raise RuntimeError(f"Failed to delete project: {e}") from e
103+
104+
@staticmethod
105+
def update_status(project_id: str, status: str, timestamp: Optional[str] = None) -> None:
106+
"""
107+
Update project status.
108+
109+
Args:
110+
project_id: Project identifier
111+
status: New status (created, indexing, ready, error)
112+
timestamp: Optional timestamp
113+
"""
114+
db_update_project_status(project_id, status, timestamp)
115+
logger.debug(f"Updated project {project_id} status to {status}")
116+
117+
@staticmethod
118+
def get_or_create(project_path: str, name: Optional[str] = None) -> Dict[str, Any]:
119+
"""Get existing project or create new one."""
120+
return db_get_or_create_project(project_path, name)
121+
122+
@staticmethod
123+
def get_stats(project_id: str) -> Dict[str, Any]:
124+
"""
125+
Get project statistics.
126+
127+
Args:
128+
project_id: Project identifier
129+
130+
Returns:
131+
Statistics dictionary with file_count and embedding_count
132+
133+
Raises:
134+
ValueError: If project not found
135+
"""
136+
project = db_get_project_by_id(project_id)
137+
if not project:
138+
raise ValueError(f"Project not found: {project_id}")
139+
140+
db_path = project["database_path"]
141+
return get_project_stats(db_path)
142+
143+
@staticmethod
144+
def is_indexed(project_id: str) -> bool:
145+
"""
146+
Check if project has been indexed.
147+
148+
Args:
149+
project_id: Project identifier
150+
151+
Returns:
152+
True if project has indexed files
153+
"""
154+
try:
155+
stats = ProjectService.get_stats(project_id)
156+
return stats.get("file_count", 0) > 0
157+
except ValueError:
158+
return False
159+
160+
@staticmethod
161+
def validate_project_ready(project_id: str) -> tuple:
162+
"""
163+
Validate that project is ready for queries.
164+
165+
Args:
166+
project_id: Project identifier
167+
168+
Returns:
169+
Tuple of (is_ready: bool, error_message: Optional[str])
170+
"""
171+
project = db_get_project_by_id(project_id)
172+
if not project:
173+
return False, "Project not found"
174+
175+
if not os.path.exists(project["path"]):
176+
return False, "Project path does not exist"
177+
178+
if not ProjectService.is_indexed(project_id):
179+
return False, "Project not indexed yet"
180+
181+
return True, None

services/search_service.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
Service layer for search operations.
3+
Handles semantic search and query processing.
4+
"""
5+
from typing import Dict, Any, List, Optional
6+
7+
from analyzer import search_semantic
8+
from db import get_project_by_id, get_project_stats
9+
from cache import search_cache
10+
from logger import get_logger
11+
import hashlib
12+
13+
logger = get_logger(__name__)
14+
15+
16+
class SearchService:
17+
"""
18+
Service layer for search operations.
19+
Provides high-level search functionality with caching.
20+
"""
21+
22+
@staticmethod
23+
def semantic_search(
24+
project_id: str,
25+
query: str,
26+
top_k: int = 5,
27+
use_cache: bool = True
28+
) -> Dict[str, Any]:
29+
"""
30+
Perform semantic search on a project.
31+
32+
Args:
33+
project_id: Project identifier
34+
query: Search query text
35+
top_k: Number of results to return
36+
use_cache: Whether to use result caching
37+
38+
Returns:
39+
Dictionary with results, project_id, and query
40+
41+
Raises:
42+
ValueError: If project not found or not indexed
43+
"""
44+
# Validate project
45+
project = get_project_by_id(project_id)
46+
if not project:
47+
raise ValueError(f"Project not found: {project_id}")
48+
49+
db_path = project["database_path"]
50+
51+
# Check if indexed
52+
stats = get_project_stats(db_path)
53+
if stats.get("file_count", 0) == 0:
54+
raise ValueError(f"Project not indexed: {project_id}")
55+
56+
# Check cache
57+
if use_cache:
58+
cache_key = SearchService._make_cache_key(project_id, query, top_k)
59+
cached = search_cache.get(cache_key)
60+
if cached is not None:
61+
logger.debug(f"Cache hit for query: {query[:50]}")
62+
return cached
63+
64+
# Perform search
65+
try:
66+
results = search_semantic(query, db_path, top_k=top_k)
67+
68+
response = {
69+
"results": results,
70+
"project_id": project_id,
71+
"query": query,
72+
"count": len(results)
73+
}
74+
75+
# Cache results
76+
if use_cache:
77+
search_cache.set(cache_key, response)
78+
79+
logger.info(f"Search completed: {len(results)} results for '{query[:50]}'")
80+
return response
81+
82+
except Exception as e:
83+
logger.error(f"Search failed: {e}")
84+
raise RuntimeError(f"Search failed: {e}") from e
85+
86+
@staticmethod
87+
def _make_cache_key(project_id: str, query: str, top_k: int) -> str:
88+
"""Generate cache key for search query."""
89+
key_str = f"{project_id}:{query}:{top_k}"
90+
key_hash = hashlib.md5(key_str.encode()).hexdigest()
91+
return f"search:{key_hash}"
92+
93+
@staticmethod
94+
def invalidate_cache(project_id: Optional[str] = None):
95+
"""
96+
Invalidate search cache.
97+
98+
Args:
99+
project_id: If provided, only invalidate for this project.
100+
If None, clear entire cache.
101+
"""
102+
if project_id is None:
103+
search_cache.clear()
104+
logger.info("Cleared entire search cache")
105+
else:
106+
# For now, just clear entire cache
107+
# Could be optimized to only clear specific project
108+
search_cache.clear()
109+
logger.info(f"Cleared search cache for project {project_id}")
110+
111+
@staticmethod
112+
def get_cache_stats() -> Dict[str, Any]:
113+
"""Get search cache statistics."""
114+
return search_cache.stats()

0 commit comments

Comments
 (0)