kodit 0.3.15__py3-none-any.whl → 0.3.17__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.
Potentially problematic release.
This version of kodit might be problematic. Click here for more details.
- kodit/_version.py +2 -2
- kodit/app.py +11 -2
- kodit/application/services/auto_indexing_service.py +16 -7
- kodit/application/services/code_indexing_application_service.py +22 -11
- kodit/application/services/indexing_worker_service.py +154 -0
- kodit/application/services/queue_service.py +52 -0
- kodit/application/services/sync_scheduler.py +10 -48
- kodit/cli.py +407 -148
- kodit/cli_utils.py +74 -0
- kodit/config.py +41 -3
- kodit/domain/entities.py +48 -1
- kodit/domain/protocols.py +29 -2
- kodit/domain/value_objects.py +13 -0
- kodit/infrastructure/api/client/__init__.py +14 -0
- kodit/infrastructure/api/client/base.py +100 -0
- kodit/infrastructure/api/client/exceptions.py +21 -0
- kodit/infrastructure/api/client/generated_endpoints.py +27 -0
- kodit/infrastructure/api/client/index_client.py +57 -0
- kodit/infrastructure/api/client/search_client.py +86 -0
- kodit/infrastructure/api/v1/dependencies.py +13 -0
- kodit/infrastructure/api/v1/routers/indexes.py +9 -4
- kodit/infrastructure/embedding/embedding_factory.py +5 -7
- kodit/infrastructure/embedding/embedding_providers/openai_embedding_provider.py +75 -13
- kodit/infrastructure/enrichment/enrichment_factory.py +5 -8
- kodit/infrastructure/enrichment/local_enrichment_provider.py +4 -1
- kodit/infrastructure/enrichment/openai_enrichment_provider.py +84 -16
- kodit/infrastructure/enrichment/utils.py +30 -0
- kodit/infrastructure/mappers/task_mapper.py +81 -0
- kodit/infrastructure/sqlalchemy/entities.py +35 -0
- kodit/infrastructure/sqlalchemy/index_repository.py +4 -4
- kodit/infrastructure/sqlalchemy/task_repository.py +81 -0
- kodit/middleware.py +1 -0
- kodit/migrations/versions/9cf0e87de578_add_queue.py +47 -0
- kodit/utils/generate_api_paths.py +135 -0
- {kodit-0.3.15.dist-info → kodit-0.3.17.dist-info}/METADATA +1 -1
- {kodit-0.3.15.dist-info → kodit-0.3.17.dist-info}/RECORD +39 -25
- {kodit-0.3.15.dist-info → kodit-0.3.17.dist-info}/WHEEL +0 -0
- {kodit-0.3.15.dist-info → kodit-0.3.17.dist-info}/entry_points.txt +0 -0
- {kodit-0.3.15.dist-info → kodit-0.3.17.dist-info}/licenses/LICENSE +0 -0
kodit/cli_utils.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Utilities for CLI commands with remote/local mode support."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from kodit.infrastructure.api.client import IndexClient, SearchClient
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from kodit.config import AppContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def with_client(f: Callable) -> Callable:
|
|
16
|
+
"""Provide appropriate client based on configuration.
|
|
17
|
+
|
|
18
|
+
This decorator automatically detects whether to run in local or remote mode
|
|
19
|
+
based on the presence of remote.server_url in the configuration. In remote
|
|
20
|
+
mode, it provides API clients. In local mode, it behaves like the existing
|
|
21
|
+
with_session decorator.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@wraps(f)
|
|
25
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
26
|
+
ctx = click.get_current_context()
|
|
27
|
+
app_context: AppContext = ctx.obj
|
|
28
|
+
|
|
29
|
+
# Auto-detect mode based on remote.server_url presence
|
|
30
|
+
if not app_context.is_remote:
|
|
31
|
+
# Local mode - use existing database session approach
|
|
32
|
+
from kodit.config import with_session
|
|
33
|
+
|
|
34
|
+
# Apply the session decorator to the original function
|
|
35
|
+
session_wrapped = with_session(f)
|
|
36
|
+
# Remove the async wrapper that with_session adds since we're already async
|
|
37
|
+
inner_func = getattr(
|
|
38
|
+
getattr(session_wrapped, "__wrapped__", session_wrapped),
|
|
39
|
+
"__wrapped__",
|
|
40
|
+
session_wrapped
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Get database session manually
|
|
44
|
+
db = await app_context.get_db()
|
|
45
|
+
async with db.session_factory() as session:
|
|
46
|
+
return await inner_func(session, *args, **kwargs)
|
|
47
|
+
else:
|
|
48
|
+
# Remote mode - use API clients
|
|
49
|
+
clients = {
|
|
50
|
+
"index_client": IndexClient(
|
|
51
|
+
base_url=app_context.remote.server_url or "",
|
|
52
|
+
api_key=app_context.remote.api_key,
|
|
53
|
+
timeout=app_context.remote.timeout,
|
|
54
|
+
max_retries=app_context.remote.max_retries,
|
|
55
|
+
verify_ssl=app_context.remote.verify_ssl,
|
|
56
|
+
),
|
|
57
|
+
"search_client": SearchClient(
|
|
58
|
+
base_url=app_context.remote.server_url or "",
|
|
59
|
+
api_key=app_context.remote.api_key,
|
|
60
|
+
timeout=app_context.remote.timeout,
|
|
61
|
+
max_retries=app_context.remote.max_retries,
|
|
62
|
+
verify_ssl=app_context.remote.verify_ssl,
|
|
63
|
+
),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# Pass clients to the command function
|
|
68
|
+
return await f(*args, clients=clients, **kwargs)
|
|
69
|
+
finally:
|
|
70
|
+
# Clean up client connections
|
|
71
|
+
for client in clients.values():
|
|
72
|
+
await client.close()
|
|
73
|
+
|
|
74
|
+
return wrapper
|
kodit/config.py
CHANGED
|
@@ -49,6 +49,14 @@ class Endpoint(BaseModel):
|
|
|
49
49
|
model: str | None = None
|
|
50
50
|
api_key: str | None = None
|
|
51
51
|
num_parallel_tasks: int | None = None
|
|
52
|
+
socket_path: str | None = Field(
|
|
53
|
+
default=None,
|
|
54
|
+
description="Unix socket path for local communication (e.g., /tmp/openai.sock)",
|
|
55
|
+
)
|
|
56
|
+
timeout: float | None = Field(
|
|
57
|
+
default=None,
|
|
58
|
+
description="Request timeout in seconds (default: 30.0)",
|
|
59
|
+
)
|
|
52
60
|
|
|
53
61
|
|
|
54
62
|
class Search(BaseModel):
|
|
@@ -103,6 +111,20 @@ class PeriodicSyncConfig(BaseModel):
|
|
|
103
111
|
)
|
|
104
112
|
|
|
105
113
|
|
|
114
|
+
class RemoteConfig(BaseModel):
|
|
115
|
+
"""Configuration for remote server connection."""
|
|
116
|
+
|
|
117
|
+
server_url: str | None = Field(
|
|
118
|
+
default=None, description="Remote Kodit server URL"
|
|
119
|
+
)
|
|
120
|
+
api_key: str | None = Field(default=None, description="API key for authentication")
|
|
121
|
+
timeout: float = Field(default=30.0, description="Request timeout in seconds")
|
|
122
|
+
max_retries: int = Field(default=3, description="Maximum retry attempts")
|
|
123
|
+
verify_ssl: bool = Field(
|
|
124
|
+
default=True, description="Verify SSL certificates"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
106
128
|
class CustomAutoIndexingEnvSource(EnvSettingsSource):
|
|
107
129
|
"""Custom environment source for parsing AutoIndexingConfig."""
|
|
108
130
|
|
|
@@ -204,6 +226,9 @@ class AppContext(BaseSettings):
|
|
|
204
226
|
default_factory=list,
|
|
205
227
|
description="Comma-separated list of valid API keys (e.g. 'key1,key2')",
|
|
206
228
|
)
|
|
229
|
+
remote: RemoteConfig = Field(
|
|
230
|
+
default_factory=RemoteConfig, description="Remote server configuration"
|
|
231
|
+
)
|
|
207
232
|
|
|
208
233
|
@field_validator("api_keys", mode="before")
|
|
209
234
|
@classmethod
|
|
@@ -226,6 +251,11 @@ class AppContext(BaseSettings):
|
|
|
226
251
|
# Call this to ensure the data dir exists for the default db location
|
|
227
252
|
self.get_data_dir()
|
|
228
253
|
|
|
254
|
+
@property
|
|
255
|
+
def is_remote(self) -> bool:
|
|
256
|
+
"""Check if running in remote mode."""
|
|
257
|
+
return self.remote.server_url is not None
|
|
258
|
+
|
|
229
259
|
def get_data_dir(self) -> Path:
|
|
230
260
|
"""Get the data directory."""
|
|
231
261
|
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -240,11 +270,19 @@ class AppContext(BaseSettings):
|
|
|
240
270
|
async def get_db(self, *, run_migrations: bool = True) -> Database:
|
|
241
271
|
"""Get the database."""
|
|
242
272
|
if self._db is None:
|
|
243
|
-
self._db =
|
|
244
|
-
if run_migrations:
|
|
245
|
-
await self._db.run_migrations(self.db_url)
|
|
273
|
+
self._db = await self.new_db(run_migrations=run_migrations)
|
|
246
274
|
return self._db
|
|
247
275
|
|
|
276
|
+
async def new_db(self, *, run_migrations: bool = True) -> Database:
|
|
277
|
+
"""Get a completely fresh connection to a database.
|
|
278
|
+
|
|
279
|
+
This is required when running tasks in a thread pool.
|
|
280
|
+
"""
|
|
281
|
+
db = Database(self.db_url)
|
|
282
|
+
if run_migrations:
|
|
283
|
+
await db.run_migrations(self.db_url)
|
|
284
|
+
return db
|
|
285
|
+
|
|
248
286
|
|
|
249
287
|
with_app_context = click.make_pass_decorator(AppContext)
|
|
250
288
|
|
kodit/domain/entities.py
CHANGED
|
@@ -4,16 +4,18 @@ import shutil
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Protocol
|
|
7
|
+
from typing import Any, Protocol
|
|
8
8
|
from urllib.parse import urlparse, urlunparse
|
|
9
9
|
|
|
10
10
|
from pydantic import AnyUrl, BaseModel
|
|
11
11
|
|
|
12
12
|
from kodit.domain.value_objects import (
|
|
13
13
|
FileProcessingStatus,
|
|
14
|
+
QueuePriority,
|
|
14
15
|
SnippetContent,
|
|
15
16
|
SnippetContentType,
|
|
16
17
|
SourceType,
|
|
18
|
+
TaskType,
|
|
17
19
|
)
|
|
18
20
|
from kodit.utils.path_utils import path_from_uri
|
|
19
21
|
|
|
@@ -274,3 +276,48 @@ class SnippetWithContext:
|
|
|
274
276
|
file: File
|
|
275
277
|
authors: list[Author]
|
|
276
278
|
snippet: Snippet
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class Task(BaseModel):
|
|
282
|
+
"""Represents an item in the queue waiting to be processed.
|
|
283
|
+
|
|
284
|
+
If the item exists, that means it is in the queue and waiting to be processed. There
|
|
285
|
+
is no status associated.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
id: str # Is a unique key to deduplicate items in the queue
|
|
289
|
+
type: TaskType # Task type
|
|
290
|
+
priority: int # Priority (higher number = higher priority)
|
|
291
|
+
payload: dict[str, Any] # Task-specific data
|
|
292
|
+
|
|
293
|
+
created_at: datetime | None = None # Is populated by repository
|
|
294
|
+
updated_at: datetime | None = None # Is populated by repository
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def create(task_type: TaskType, priority: int, payload: dict[str, Any]) -> "Task":
|
|
298
|
+
"""Create a task."""
|
|
299
|
+
return Task(
|
|
300
|
+
id=Task._create_id(task_type, payload),
|
|
301
|
+
type=task_type,
|
|
302
|
+
priority=priority,
|
|
303
|
+
payload=payload,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _create_id(task_type: TaskType, payload: dict[str, Any]) -> str:
|
|
308
|
+
"""Create a unique id for a task."""
|
|
309
|
+
if task_type == TaskType.INDEX_UPDATE:
|
|
310
|
+
return str(payload["index_id"])
|
|
311
|
+
|
|
312
|
+
raise ValueError(f"Unknown task type: {task_type}")
|
|
313
|
+
|
|
314
|
+
@staticmethod
|
|
315
|
+
def create_index_update_task(
|
|
316
|
+
index_id: int, priority: QueuePriority = QueuePriority.USER_INITIATED
|
|
317
|
+
) -> "Task":
|
|
318
|
+
"""Create an index update task."""
|
|
319
|
+
return Task.create(
|
|
320
|
+
task_type=TaskType.INDEX_UPDATE,
|
|
321
|
+
priority=priority.value,
|
|
322
|
+
payload={"index_id": index_id},
|
|
323
|
+
)
|
kodit/domain/protocols.py
CHANGED
|
@@ -5,8 +5,35 @@ from typing import Protocol
|
|
|
5
5
|
|
|
6
6
|
from pydantic import AnyUrl
|
|
7
7
|
|
|
8
|
-
from kodit.domain.entities import Index, Snippet, SnippetWithContext, WorkingCopy
|
|
9
|
-
from kodit.domain.value_objects import MultiSearchRequest
|
|
8
|
+
from kodit.domain.entities import Index, Snippet, SnippetWithContext, Task, WorkingCopy
|
|
9
|
+
from kodit.domain.value_objects import MultiSearchRequest, TaskType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TaskRepository(Protocol):
|
|
13
|
+
"""Repository interface for Task entities."""
|
|
14
|
+
|
|
15
|
+
async def add(
|
|
16
|
+
self,
|
|
17
|
+
task: Task,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Add a task."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
async def get(self, task_id: str) -> Task | None:
|
|
23
|
+
"""Get a task by ID."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
async def take(self) -> Task | None:
|
|
27
|
+
"""Take a task for processing."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
async def update(self, task: Task) -> None:
|
|
31
|
+
"""Update a task."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
async def list(self, task_type: TaskType | None = None) -> list[Task]:
|
|
35
|
+
"""List tasks with optional status filter."""
|
|
36
|
+
...
|
|
10
37
|
|
|
11
38
|
|
|
12
39
|
class IndexRepository(Protocol):
|
kodit/domain/value_objects.py
CHANGED
|
@@ -649,3 +649,16 @@ class FunctionDefinition:
|
|
|
649
649
|
qualified_name: str
|
|
650
650
|
start_byte: int
|
|
651
651
|
end_byte: int
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class TaskType(Enum):
|
|
655
|
+
"""Task type."""
|
|
656
|
+
|
|
657
|
+
INDEX_UPDATE = 1
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class QueuePriority(IntEnum):
|
|
661
|
+
"""Queue priority."""
|
|
662
|
+
|
|
663
|
+
BACKGROUND = 10
|
|
664
|
+
USER_INITIATED = 50
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""API client for remote Kodit server communication."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseAPIClient
|
|
4
|
+
from .exceptions import AuthenticationError, KoditAPIError
|
|
5
|
+
from .index_client import IndexClient
|
|
6
|
+
from .search_client import SearchClient
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AuthenticationError",
|
|
10
|
+
"BaseAPIClient",
|
|
11
|
+
"IndexClient",
|
|
12
|
+
"KoditAPIError",
|
|
13
|
+
"SearchClient",
|
|
14
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Base HTTP client for Kodit API communication."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .exceptions import AuthenticationError, KoditAPIError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseAPIClient:
|
|
12
|
+
"""Base API client with authentication and retry logic."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
base_url: str,
|
|
17
|
+
api_key: str | None = None,
|
|
18
|
+
timeout: float = 30.0,
|
|
19
|
+
max_retries: int = 3,
|
|
20
|
+
*,
|
|
21
|
+
verify_ssl: bool = True,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Initialize the API client.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
base_url: Base URL of the Kodit server
|
|
27
|
+
api_key: API key for authentication
|
|
28
|
+
timeout: Request timeout in seconds
|
|
29
|
+
max_retries: Maximum retry attempts
|
|
30
|
+
verify_ssl: Whether to verify SSL certificates
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
self.base_url = base_url.rstrip("/")
|
|
34
|
+
self.api_key = api_key
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
self.max_retries = max_retries
|
|
37
|
+
self.verify_ssl = verify_ssl
|
|
38
|
+
self._client = self._create_client()
|
|
39
|
+
|
|
40
|
+
def _create_client(self) -> httpx.AsyncClient:
|
|
41
|
+
"""Create the HTTP client with proper configuration."""
|
|
42
|
+
headers = {}
|
|
43
|
+
if self.api_key:
|
|
44
|
+
headers["X-API-Key"] = self.api_key
|
|
45
|
+
|
|
46
|
+
return httpx.AsyncClient(
|
|
47
|
+
base_url=self.base_url,
|
|
48
|
+
headers=headers,
|
|
49
|
+
timeout=httpx.Timeout(self.timeout),
|
|
50
|
+
verify=self.verify_ssl,
|
|
51
|
+
follow_redirects=True,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
async def _request(
|
|
55
|
+
self,
|
|
56
|
+
method: str,
|
|
57
|
+
path: str,
|
|
58
|
+
**kwargs: Any,
|
|
59
|
+
) -> httpx.Response:
|
|
60
|
+
"""Make HTTP request with retry logic.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
method: HTTP method (GET, POST, etc.)
|
|
64
|
+
path: API endpoint path
|
|
65
|
+
**kwargs: Additional arguments passed to httpx
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
HTTP response object
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
AuthenticationError: If authentication fails
|
|
72
|
+
KoditAPIError: For other API errors
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
url = f"{self.base_url}{path}"
|
|
76
|
+
|
|
77
|
+
for attempt in range(self.max_retries):
|
|
78
|
+
try:
|
|
79
|
+
response = await self._client.request(method, url, **kwargs)
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
except httpx.HTTPStatusError as e:
|
|
82
|
+
if e.response.status_code == 401:
|
|
83
|
+
raise AuthenticationError("Invalid API key") from e
|
|
84
|
+
if e.response.status_code >= 500 and attempt < self.max_retries - 1:
|
|
85
|
+
await asyncio.sleep(2**attempt) # Exponential backoff
|
|
86
|
+
continue
|
|
87
|
+
raise KoditAPIError(f"API request failed: {e}") from e
|
|
88
|
+
except httpx.RequestError as e:
|
|
89
|
+
if attempt < self.max_retries - 1:
|
|
90
|
+
await asyncio.sleep(2**attempt)
|
|
91
|
+
continue
|
|
92
|
+
raise KoditAPIError(f"Connection error: {e}") from e
|
|
93
|
+
else:
|
|
94
|
+
return response
|
|
95
|
+
|
|
96
|
+
raise KoditAPIError(f"Max retries ({self.max_retries}) exceeded")
|
|
97
|
+
|
|
98
|
+
async def close(self) -> None:
|
|
99
|
+
"""Close the HTTP client."""
|
|
100
|
+
await self._client.aclose()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Exceptions for Kodit API client operations."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class KoditAPIError(Exception):
|
|
5
|
+
"""Base exception for Kodit API errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthenticationError(KoditAPIError):
|
|
10
|
+
"""Authentication failed with the API server."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class KoditConnectionError(KoditAPIError):
|
|
15
|
+
"""Connection to API server failed."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ServerError(KoditAPIError):
|
|
20
|
+
"""Server returned an error response."""
|
|
21
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""API endpoint constants generated from OpenAPI specification.
|
|
2
|
+
|
|
3
|
+
This file is auto-generated. Do not edit manually.
|
|
4
|
+
Run `make generate-api-paths` to regenerate.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIEndpoints:
|
|
11
|
+
"""API endpoint constants extracted from OpenAPI specification."""
|
|
12
|
+
|
|
13
|
+
# /api/v1/indexes
|
|
14
|
+
API_V1_INDEXES: Final[str] = "/api/v1/indexes"
|
|
15
|
+
|
|
16
|
+
# /api/v1/indexes/{index_id}
|
|
17
|
+
API_V1_INDEXES_INDEX_ID: Final[str] = "/api/v1/indexes/{index_id}"
|
|
18
|
+
|
|
19
|
+
# /api/v1/search
|
|
20
|
+
API_V1_SEARCH: Final[str] = "/api/v1/search"
|
|
21
|
+
|
|
22
|
+
# /healthz
|
|
23
|
+
HEALTHZ: Final[str] = "/healthz"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Generated from: openapi.json
|
|
27
|
+
# Total endpoints: 4
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Index operations API client for Kodit server."""
|
|
2
|
+
|
|
3
|
+
from kodit.infrastructure.api.v1.schemas.index import (
|
|
4
|
+
IndexCreateAttributes,
|
|
5
|
+
IndexCreateData,
|
|
6
|
+
IndexCreateRequest,
|
|
7
|
+
IndexData,
|
|
8
|
+
IndexListResponse,
|
|
9
|
+
IndexResponse,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .base import BaseAPIClient
|
|
13
|
+
from .exceptions import KoditAPIError
|
|
14
|
+
from .generated_endpoints import APIEndpoints
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IndexClient(BaseAPIClient):
|
|
18
|
+
"""API client for index operations."""
|
|
19
|
+
|
|
20
|
+
async def list_indexes(self) -> list[IndexData]:
|
|
21
|
+
"""List all indexes."""
|
|
22
|
+
response = await self._request("GET", APIEndpoints.API_V1_INDEXES)
|
|
23
|
+
data = IndexListResponse.model_validate_json(response.text)
|
|
24
|
+
return data.data
|
|
25
|
+
|
|
26
|
+
async def create_index(self, uri: str) -> IndexData:
|
|
27
|
+
"""Create a new index."""
|
|
28
|
+
request = IndexCreateRequest(
|
|
29
|
+
data=IndexCreateData(
|
|
30
|
+
type="index", attributes=IndexCreateAttributes(uri=uri)
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
response = await self._request(
|
|
34
|
+
"POST", APIEndpoints.API_V1_INDEXES, json=request.model_dump()
|
|
35
|
+
)
|
|
36
|
+
result = IndexResponse.model_validate_json(response.text)
|
|
37
|
+
return result.data
|
|
38
|
+
|
|
39
|
+
async def get_index(self, index_id: str) -> IndexData | None:
|
|
40
|
+
"""Get index by ID."""
|
|
41
|
+
try:
|
|
42
|
+
response = await self._request(
|
|
43
|
+
"GET", APIEndpoints.API_V1_INDEXES_INDEX_ID.format(index_id=index_id)
|
|
44
|
+
)
|
|
45
|
+
result = IndexResponse.model_validate_json(response.text)
|
|
46
|
+
except KoditAPIError as e:
|
|
47
|
+
if "404" in str(e):
|
|
48
|
+
return None
|
|
49
|
+
raise
|
|
50
|
+
else:
|
|
51
|
+
return result.data
|
|
52
|
+
|
|
53
|
+
async def delete_index(self, index_id: str) -> None:
|
|
54
|
+
"""Delete an index."""
|
|
55
|
+
await self._request(
|
|
56
|
+
"DELETE", APIEndpoints.API_V1_INDEXES_INDEX_ID.format(index_id=index_id)
|
|
57
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Search operations API client for Kodit server."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from kodit.infrastructure.api.v1.schemas.search import (
|
|
6
|
+
SearchAttributes,
|
|
7
|
+
SearchData,
|
|
8
|
+
SearchFilters,
|
|
9
|
+
SearchRequest,
|
|
10
|
+
SearchResponse,
|
|
11
|
+
SnippetData,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .base import BaseAPIClient
|
|
15
|
+
from .generated_endpoints import APIEndpoints
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SearchClient(BaseAPIClient):
|
|
19
|
+
"""API client for search operations."""
|
|
20
|
+
|
|
21
|
+
async def search( # noqa: PLR0913
|
|
22
|
+
self,
|
|
23
|
+
keywords: list[str] | None = None,
|
|
24
|
+
code_query: str | None = None,
|
|
25
|
+
text_query: str | None = None,
|
|
26
|
+
limit: int = 10,
|
|
27
|
+
languages: list[str] | None = None,
|
|
28
|
+
authors: list[str] | None = None,
|
|
29
|
+
start_date: datetime | None = None,
|
|
30
|
+
end_date: datetime | None = None,
|
|
31
|
+
sources: list[str] | None = None,
|
|
32
|
+
file_patterns: list[str] | None = None,
|
|
33
|
+
) -> list[SnippetData]:
|
|
34
|
+
"""Search for code snippets.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
keywords: Keywords to search for
|
|
38
|
+
code_query: Code search query
|
|
39
|
+
text_query: Text search query
|
|
40
|
+
limit: Maximum number of results
|
|
41
|
+
languages: Programming languages to filter by
|
|
42
|
+
authors: Authors to filter by
|
|
43
|
+
start_date: Filter snippets created after this date
|
|
44
|
+
end_date: Filter snippets created before this date
|
|
45
|
+
sources: Source repositories to filter by
|
|
46
|
+
file_patterns: File path patterns to filter by
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of matching snippets
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
KoditAPIError: If the request fails
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
filters = None
|
|
56
|
+
if any([languages, authors, start_date, end_date, sources, file_patterns]):
|
|
57
|
+
filters = SearchFilters(
|
|
58
|
+
languages=languages,
|
|
59
|
+
authors=authors,
|
|
60
|
+
start_date=start_date,
|
|
61
|
+
end_date=end_date,
|
|
62
|
+
sources=sources,
|
|
63
|
+
file_patterns=file_patterns,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
request = SearchRequest(
|
|
67
|
+
data=SearchData(
|
|
68
|
+
type="search",
|
|
69
|
+
attributes=SearchAttributes(
|
|
70
|
+
keywords=keywords,
|
|
71
|
+
code=code_query,
|
|
72
|
+
text=text_query,
|
|
73
|
+
limit=limit,
|
|
74
|
+
filters=filters,
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
response = await self._request(
|
|
80
|
+
"POST",
|
|
81
|
+
APIEndpoints.API_V1_SEARCH,
|
|
82
|
+
json=request.model_dump(exclude_none=True),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
result = SearchResponse.model_validate_json(response.text)
|
|
86
|
+
return result.data
|
|
@@ -12,6 +12,7 @@ from kodit.application.factories.code_indexing_factory import (
|
|
|
12
12
|
from kodit.application.services.code_indexing_application_service import (
|
|
13
13
|
CodeIndexingApplicationService,
|
|
14
14
|
)
|
|
15
|
+
from kodit.application.services.queue_service import QueueService
|
|
15
16
|
from kodit.config import AppContext
|
|
16
17
|
from kodit.domain.services.index_query_service import IndexQueryService
|
|
17
18
|
from kodit.infrastructure.indexing.fusion_service import ReciprocalRankFusionService
|
|
@@ -68,3 +69,15 @@ async def get_indexing_app_service(
|
|
|
68
69
|
IndexingAppServiceDep = Annotated[
|
|
69
70
|
CodeIndexingApplicationService, Depends(get_indexing_app_service)
|
|
70
71
|
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def get_queue_service(
|
|
75
|
+
session: DBSessionDep,
|
|
76
|
+
) -> QueueService:
|
|
77
|
+
"""Get queue service dependency."""
|
|
78
|
+
return QueueService(
|
|
79
|
+
session=session,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
QueueServiceDep = Annotated[QueueService, Depends(get_queue_service)]
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""Index management router for the REST API."""
|
|
2
2
|
|
|
3
|
-
from fastapi import APIRouter,
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
4
4
|
|
|
5
|
+
from kodit.domain.entities import Task
|
|
6
|
+
from kodit.domain.value_objects import QueuePriority
|
|
5
7
|
from kodit.infrastructure.api.middleware.auth import api_key_auth
|
|
6
8
|
from kodit.infrastructure.api.v1.dependencies import (
|
|
7
9
|
IndexingAppServiceDep,
|
|
8
10
|
IndexQueryServiceDep,
|
|
11
|
+
QueueServiceDep,
|
|
9
12
|
)
|
|
10
13
|
from kodit.infrastructure.api.v1.schemas.index import (
|
|
11
14
|
IndexAttributes,
|
|
@@ -52,15 +55,17 @@ async def list_indexes(
|
|
|
52
55
|
@router.post("", status_code=202)
|
|
53
56
|
async def create_index(
|
|
54
57
|
request: IndexCreateRequest,
|
|
55
|
-
background_tasks: BackgroundTasks,
|
|
56
58
|
app_service: IndexingAppServiceDep,
|
|
59
|
+
queue_service: QueueServiceDep,
|
|
57
60
|
) -> IndexResponse:
|
|
58
61
|
"""Create a new index and start async indexing."""
|
|
59
62
|
# Create index using the application service
|
|
60
63
|
index = await app_service.create_index_from_uri(request.data.attributes.uri)
|
|
61
64
|
|
|
62
|
-
#
|
|
63
|
-
|
|
65
|
+
# Add the indexing task to the queue
|
|
66
|
+
await queue_service.enqueue_task(
|
|
67
|
+
Task.create_index_update_task(index.id, QueuePriority.USER_INITIATED)
|
|
68
|
+
)
|
|
64
69
|
|
|
65
70
|
return IndexResponse(
|
|
66
71
|
data=IndexData(
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Factory for creating embedding services with DDD architecture."""
|
|
2
2
|
|
|
3
|
-
from openai import AsyncOpenAI
|
|
4
3
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
4
|
|
|
6
5
|
from kodit.config import AppContext, Endpoint
|
|
@@ -48,15 +47,14 @@ def embedding_domain_service_factory(
|
|
|
48
47
|
endpoint = _get_endpoint_configuration(app_context)
|
|
49
48
|
if endpoint and endpoint.type == "openai":
|
|
50
49
|
log_event("kodit.embedding", {"provider": "openai"})
|
|
50
|
+
# Use new httpx-based provider with socket support
|
|
51
51
|
embedding_provider = OpenAIEmbeddingProvider(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
base_url=endpoint.base_url or "https://api.openai.com/v1",
|
|
55
|
-
timeout=10,
|
|
56
|
-
max_retries=2,
|
|
57
|
-
),
|
|
52
|
+
api_key=endpoint.api_key,
|
|
53
|
+
base_url=endpoint.base_url or "https://api.openai.com/v1",
|
|
58
54
|
model_name=endpoint.model or "text-embedding-3-small",
|
|
59
55
|
num_parallel_tasks=endpoint.num_parallel_tasks or OPENAI_NUM_PARALLEL_TASKS,
|
|
56
|
+
socket_path=endpoint.socket_path,
|
|
57
|
+
timeout=endpoint.timeout or 30.0,
|
|
60
58
|
)
|
|
61
59
|
else:
|
|
62
60
|
log_event("kodit.embedding", {"provider": "local"})
|