kodit 0.5.0__py3-none-any.whl → 0.5.2__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 +10 -12
- kodit/application/factories/server_factory.py +78 -11
- kodit/application/services/commit_indexing_application_service.py +188 -31
- kodit/application/services/enrichment_query_service.py +95 -0
- kodit/config.py +3 -3
- kodit/domain/enrichments/__init__.py +1 -0
- kodit/domain/enrichments/architecture/__init__.py +1 -0
- kodit/domain/enrichments/architecture/architecture.py +20 -0
- kodit/domain/enrichments/architecture/physical/__init__.py +1 -0
- kodit/domain/enrichments/architecture/physical/discovery_notes.py +14 -0
- kodit/domain/enrichments/architecture/physical/formatter.py +11 -0
- kodit/domain/enrichments/architecture/physical/physical.py +17 -0
- kodit/domain/enrichments/development/__init__.py +1 -0
- kodit/domain/enrichments/development/development.py +18 -0
- kodit/domain/enrichments/development/snippet/__init__.py +1 -0
- kodit/domain/enrichments/development/snippet/snippet.py +21 -0
- kodit/domain/enrichments/enricher.py +17 -0
- kodit/domain/enrichments/enrichment.py +39 -0
- kodit/domain/enrichments/request.py +12 -0
- kodit/domain/enrichments/response.py +11 -0
- kodit/domain/enrichments/usage/__init__.py +1 -0
- kodit/domain/enrichments/usage/api_docs.py +19 -0
- kodit/domain/enrichments/usage/usage.py +18 -0
- kodit/domain/protocols.py +7 -6
- kodit/domain/services/enrichment_service.py +9 -30
- kodit/domain/services/physical_architecture_service.py +182 -0
- kodit/domain/tracking/__init__.py +1 -0
- kodit/domain/tracking/resolution_service.py +81 -0
- kodit/domain/tracking/trackable.py +21 -0
- kodit/domain/value_objects.py +6 -23
- kodit/infrastructure/api/v1/dependencies.py +15 -0
- kodit/infrastructure/api/v1/routers/commits.py +81 -0
- kodit/infrastructure/api/v1/routers/repositories.py +99 -0
- kodit/infrastructure/api/v1/schemas/enrichment.py +29 -0
- kodit/infrastructure/cloning/git/git_python_adaptor.py +71 -4
- kodit/infrastructure/enricher/__init__.py +1 -0
- kodit/infrastructure/enricher/enricher_factory.py +53 -0
- kodit/infrastructure/{enrichment/litellm_enrichment_provider.py → enricher/litellm_enricher.py} +20 -33
- kodit/infrastructure/{enrichment/local_enrichment_provider.py → enricher/local_enricher.py} +19 -24
- kodit/infrastructure/enricher/null_enricher.py +36 -0
- kodit/infrastructure/mappers/enrichment_mapper.py +83 -0
- kodit/infrastructure/mappers/snippet_mapper.py +20 -22
- kodit/infrastructure/physical_architecture/__init__.py +1 -0
- kodit/infrastructure/physical_architecture/detectors/__init__.py +1 -0
- kodit/infrastructure/physical_architecture/detectors/docker_compose_detector.py +336 -0
- kodit/infrastructure/physical_architecture/formatters/__init__.py +1 -0
- kodit/infrastructure/physical_architecture/formatters/narrative_formatter.py +149 -0
- kodit/infrastructure/slicing/api_doc_extractor.py +836 -0
- kodit/infrastructure/slicing/ast_analyzer.py +1128 -0
- kodit/infrastructure/slicing/slicer.py +56 -391
- kodit/infrastructure/sqlalchemy/enrichment_v2_repository.py +118 -0
- kodit/infrastructure/sqlalchemy/entities.py +46 -38
- kodit/infrastructure/sqlalchemy/git_branch_repository.py +22 -11
- kodit/infrastructure/sqlalchemy/git_commit_repository.py +23 -14
- kodit/infrastructure/sqlalchemy/git_repository.py +27 -17
- kodit/infrastructure/sqlalchemy/git_tag_repository.py +22 -11
- kodit/infrastructure/sqlalchemy/snippet_v2_repository.py +101 -106
- kodit/migrations/versions/19f8c7faf8b9_add_generic_enrichment_type.py +260 -0
- kodit/utils/dump_config.py +361 -0
- kodit/utils/dump_openapi.py +5 -6
- {kodit-0.5.0.dist-info → kodit-0.5.2.dist-info}/METADATA +1 -1
- {kodit-0.5.0.dist-info → kodit-0.5.2.dist-info}/RECORD +67 -32
- kodit/infrastructure/enrichment/__init__.py +0 -1
- kodit/infrastructure/enrichment/enrichment_factory.py +0 -52
- kodit/infrastructure/enrichment/null_enrichment_provider.py +0 -19
- /kodit/infrastructure/{enrichment → enricher}/utils.py +0 -0
- {kodit-0.5.0.dist-info → kodit-0.5.2.dist-info}/WHEEL +0 -0
- {kodit-0.5.0.dist-info → kodit-0.5.2.dist-info}/entry_points.txt +0 -0
- {kodit-0.5.0.dist-info → kodit-0.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,6 +13,9 @@ from kodit.application.services.code_search_application_service import (
|
|
|
13
13
|
from kodit.application.services.commit_indexing_application_service import (
|
|
14
14
|
CommitIndexingApplicationService,
|
|
15
15
|
)
|
|
16
|
+
from kodit.application.services.enrichment_query_service import (
|
|
17
|
+
EnrichmentQueryService,
|
|
18
|
+
)
|
|
16
19
|
from kodit.application.services.queue_service import QueueService
|
|
17
20
|
from kodit.config import AppContext
|
|
18
21
|
from kodit.domain.protocols import (
|
|
@@ -155,3 +158,15 @@ async def get_code_search_app_service(
|
|
|
155
158
|
CodeSearchAppServiceDep = Annotated[
|
|
156
159
|
CodeSearchApplicationService, Depends(get_code_search_app_service)
|
|
157
160
|
]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def get_enrichment_query_service(
|
|
164
|
+
server_factory: ServerFactoryDep,
|
|
165
|
+
) -> EnrichmentQueryService:
|
|
166
|
+
"""Get enrichment query service dependency."""
|
|
167
|
+
return server_factory.enrichment_query_service()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
EnrichmentQueryServiceDep = Annotated[
|
|
171
|
+
EnrichmentQueryService, Depends(get_enrichment_query_service)
|
|
172
|
+
]
|
|
@@ -22,6 +22,11 @@ from kodit.infrastructure.api.v1.schemas.commit import (
|
|
|
22
22
|
FileListResponse,
|
|
23
23
|
FileResponse,
|
|
24
24
|
)
|
|
25
|
+
from kodit.infrastructure.api.v1.schemas.enrichment import (
|
|
26
|
+
EnrichmentAttributes,
|
|
27
|
+
EnrichmentData,
|
|
28
|
+
EnrichmentListResponse,
|
|
29
|
+
)
|
|
25
30
|
from kodit.infrastructure.api.v1.schemas.snippet import (
|
|
26
31
|
EnrichmentSchema,
|
|
27
32
|
GitFileSchema,
|
|
@@ -269,3 +274,79 @@ async def list_commit_embeddings(
|
|
|
269
274
|
for embedding in embeddings
|
|
270
275
|
]
|
|
271
276
|
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@router.get(
|
|
280
|
+
"/{repo_id}/commits/{commit_sha}/enrichments",
|
|
281
|
+
summary="List commit enrichments",
|
|
282
|
+
responses={404: {"description": "Repository or commit not found"}},
|
|
283
|
+
)
|
|
284
|
+
async def list_commit_enrichments(
|
|
285
|
+
repo_id: str, # noqa: ARG001
|
|
286
|
+
commit_sha: str,
|
|
287
|
+
server_factory: ServerFactoryDep,
|
|
288
|
+
) -> EnrichmentListResponse:
|
|
289
|
+
"""List all enrichments for a specific commit."""
|
|
290
|
+
# TODO(Phil): Should use repo too, it's confusing to the user when they specify the
|
|
291
|
+
# wrong commit and another repo. It's like they are seeing results from the other
|
|
292
|
+
# repo.
|
|
293
|
+
enrichment_v2_repository = server_factory.enrichment_v2_repository()
|
|
294
|
+
enrichments = await enrichment_v2_repository.enrichments_for_entity_type(
|
|
295
|
+
entity_type="git_commit",
|
|
296
|
+
entity_ids=[commit_sha],
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return EnrichmentListResponse(
|
|
300
|
+
data=[
|
|
301
|
+
EnrichmentData(
|
|
302
|
+
type="enrichment",
|
|
303
|
+
id=str(enrichment.id),
|
|
304
|
+
attributes=EnrichmentAttributes(
|
|
305
|
+
type=enrichment.type,
|
|
306
|
+
subtype=enrichment.subtype,
|
|
307
|
+
content=enrichment.content,
|
|
308
|
+
created_at=enrichment.created_at,
|
|
309
|
+
updated_at=enrichment.updated_at,
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
for enrichment in enrichments
|
|
313
|
+
]
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@router.delete(
|
|
318
|
+
"/{repo_id}/commits/{commit_sha}/enrichments",
|
|
319
|
+
summary="Delete all commit enrichments",
|
|
320
|
+
responses={404: {"description": "Commit not found"}},
|
|
321
|
+
status_code=204,
|
|
322
|
+
)
|
|
323
|
+
async def delete_all_commit_enrichments(
|
|
324
|
+
repo_id: str, # noqa: ARG001
|
|
325
|
+
commit_sha: str,
|
|
326
|
+
server_factory: ServerFactoryDep,
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Delete all enrichments for a specific commit."""
|
|
329
|
+
enrichment_v2_repository = server_factory.enrichment_v2_repository()
|
|
330
|
+
await enrichment_v2_repository.bulk_delete_enrichments(
|
|
331
|
+
entity_type="git_commit",
|
|
332
|
+
entity_ids=[commit_sha],
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@router.delete(
|
|
337
|
+
"/{repo_id}/commits/{commit_sha}/enrichments/{enrichment_id}",
|
|
338
|
+
summary="Delete commit enrichment",
|
|
339
|
+
responses={404: {"description": "Enrichment not found"}},
|
|
340
|
+
status_code=204,
|
|
341
|
+
)
|
|
342
|
+
async def delete_commit_enrichment(
|
|
343
|
+
repo_id: str, # noqa: ARG001
|
|
344
|
+
commit_sha: str, # noqa: ARG001
|
|
345
|
+
enrichment_id: int,
|
|
346
|
+
server_factory: ServerFactoryDep,
|
|
347
|
+
) -> None:
|
|
348
|
+
"""Delete a specific enrichment for a commit."""
|
|
349
|
+
enrichment_v2_repository = server_factory.enrichment_v2_repository()
|
|
350
|
+
deleted = await enrichment_v2_repository.delete_enrichment(enrichment_id)
|
|
351
|
+
if not deleted:
|
|
352
|
+
raise HTTPException(status_code=404, detail="Enrichment not found")
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
from fastapi import APIRouter, Depends, HTTPException
|
|
4
4
|
|
|
5
|
+
from kodit.domain.tracking.trackable import Trackable, TrackableReferenceType
|
|
5
6
|
from kodit.infrastructure.api.middleware.auth import api_key_auth
|
|
6
7
|
from kodit.infrastructure.api.v1.dependencies import (
|
|
7
8
|
CommitIndexingAppServiceDep,
|
|
9
|
+
EnrichmentQueryServiceDep,
|
|
8
10
|
GitBranchRepositoryDep,
|
|
9
11
|
GitCommitRepositoryDep,
|
|
10
12
|
GitRepositoryDep,
|
|
11
13
|
GitTagRepositoryDep,
|
|
12
14
|
TaskStatusQueryServiceDep,
|
|
13
15
|
)
|
|
16
|
+
from kodit.infrastructure.api.v1.schemas.enrichment import (
|
|
17
|
+
EnrichmentAttributes,
|
|
18
|
+
EnrichmentData,
|
|
19
|
+
EnrichmentListResponse,
|
|
20
|
+
)
|
|
14
21
|
from kodit.infrastructure.api.v1.schemas.repository import (
|
|
15
22
|
RepositoryBranchData,
|
|
16
23
|
RepositoryCommitData,
|
|
@@ -259,6 +266,98 @@ async def get_repository_tag(
|
|
|
259
266
|
)
|
|
260
267
|
|
|
261
268
|
|
|
269
|
+
@router.get(
|
|
270
|
+
"/{repo_id}/enrichments",
|
|
271
|
+
summary="List latest repository enrichments",
|
|
272
|
+
responses={404: {"description": "Repository not found"}},
|
|
273
|
+
)
|
|
274
|
+
async def list_repository_enrichments( # noqa: PLR0913
|
|
275
|
+
repo_id: str,
|
|
276
|
+
git_repository: GitRepositoryDep,
|
|
277
|
+
enrichment_query_service: EnrichmentQueryServiceDep,
|
|
278
|
+
ref_type: str = "branch",
|
|
279
|
+
ref_name: str | None = None,
|
|
280
|
+
enrichment_type: str | None = None,
|
|
281
|
+
limit: int = 10,
|
|
282
|
+
) -> EnrichmentListResponse:
|
|
283
|
+
"""List the most recent enrichments for a repository.
|
|
284
|
+
|
|
285
|
+
Query parameters:
|
|
286
|
+
- ref_type: Type of reference (branch, tag, or commit_sha). Defaults to "branch".
|
|
287
|
+
- ref_name: Name of the reference. For branches, defaults to the tracking branch.
|
|
288
|
+
- enrichment_type: Optional filter for specific enrichment type.
|
|
289
|
+
- limit: Maximum number of enrichments to return. Defaults to 10.
|
|
290
|
+
"""
|
|
291
|
+
# Get repository
|
|
292
|
+
repo = await git_repository.get_by_id(int(repo_id))
|
|
293
|
+
if not repo:
|
|
294
|
+
raise HTTPException(status_code=404, detail="Repository not found")
|
|
295
|
+
|
|
296
|
+
# Determine the reference to track
|
|
297
|
+
if ref_name is None:
|
|
298
|
+
if ref_type == "branch":
|
|
299
|
+
# Default to tracking branch
|
|
300
|
+
if not repo.tracking_branch:
|
|
301
|
+
raise HTTPException(
|
|
302
|
+
status_code=400, detail="No tracking branch configured"
|
|
303
|
+
)
|
|
304
|
+
ref_name = repo.tracking_branch.name
|
|
305
|
+
else:
|
|
306
|
+
raise HTTPException(
|
|
307
|
+
status_code=400,
|
|
308
|
+
detail="ref_name is required for tag and commit_sha references",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Parse ref_type
|
|
312
|
+
try:
|
|
313
|
+
trackable_type = TrackableReferenceType(ref_type)
|
|
314
|
+
except ValueError:
|
|
315
|
+
raise HTTPException(
|
|
316
|
+
status_code=400,
|
|
317
|
+
detail=f"Invalid ref_type: {ref_type}. Must be branch, tag, or commit_sha",
|
|
318
|
+
) from None
|
|
319
|
+
|
|
320
|
+
# Create trackable
|
|
321
|
+
trackable = Trackable(
|
|
322
|
+
type=trackable_type, identifier=ref_name, repo_id=int(repo_id)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Find the latest enriched commit
|
|
326
|
+
enriched_commit = await enrichment_query_service.find_latest_enriched_commit(
|
|
327
|
+
trackable=trackable,
|
|
328
|
+
enrichment_type=enrichment_type,
|
|
329
|
+
max_commits_to_check=limit * 10, # Check more commits to find enriched ones
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# If no enriched commit found, return empty list
|
|
333
|
+
if not enriched_commit:
|
|
334
|
+
return EnrichmentListResponse(data=[])
|
|
335
|
+
|
|
336
|
+
# Get enrichments for the commit
|
|
337
|
+
enrichments = await enrichment_query_service.get_enrichments_for_commit(
|
|
338
|
+
commit_sha=enriched_commit,
|
|
339
|
+
enrichment_type=enrichment_type,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Map enrichments to API response format
|
|
343
|
+
enrichment_data = [
|
|
344
|
+
EnrichmentData(
|
|
345
|
+
type="enrichment",
|
|
346
|
+
id=str(enrichment.id) if enrichment.id else "0",
|
|
347
|
+
attributes=EnrichmentAttributes(
|
|
348
|
+
type=enrichment.type,
|
|
349
|
+
subtype=enrichment.subtype,
|
|
350
|
+
content=enrichment.content,
|
|
351
|
+
created_at=enrichment.created_at,
|
|
352
|
+
updated_at=enrichment.updated_at,
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
for enrichment in enrichments
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
return EnrichmentListResponse(data=enrichment_data)
|
|
359
|
+
|
|
360
|
+
|
|
262
361
|
@router.delete(
|
|
263
362
|
"/{repo_id}",
|
|
264
363
|
status_code=204,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Enrichment JSON-API schemas."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EnrichmentAttributes(BaseModel):
|
|
9
|
+
"""Enrichment attributes following JSON-API spec."""
|
|
10
|
+
|
|
11
|
+
type: str
|
|
12
|
+
subtype: str | None
|
|
13
|
+
content: str
|
|
14
|
+
created_at: datetime | None
|
|
15
|
+
updated_at: datetime | None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EnrichmentData(BaseModel):
|
|
19
|
+
"""Enrichment data following JSON-API spec."""
|
|
20
|
+
|
|
21
|
+
type: str = "enrichment"
|
|
22
|
+
id: str
|
|
23
|
+
attributes: EnrichmentAttributes
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EnrichmentListResponse(BaseModel):
|
|
27
|
+
"""Enrichment list response following JSON-API spec."""
|
|
28
|
+
|
|
29
|
+
data: list[EnrichmentData]
|
|
@@ -101,8 +101,11 @@ class GitPythonAdapter(GitAdapter):
|
|
|
101
101
|
|
|
102
102
|
await asyncio.get_event_loop().run_in_executor(self.executor, _clone)
|
|
103
103
|
|
|
104
|
-
async def
|
|
105
|
-
"""Checkout a specific commit
|
|
104
|
+
async def _checkout_commit(self, local_path: Path, commit_sha: str) -> None:
|
|
105
|
+
"""Checkout a specific commit internally.
|
|
106
|
+
|
|
107
|
+
Private method - external callers should not mutate repository state directly.
|
|
108
|
+
"""
|
|
106
109
|
|
|
107
110
|
def _checkout() -> None:
|
|
108
111
|
try:
|
|
@@ -116,6 +119,52 @@ class GitPythonAdapter(GitAdapter):
|
|
|
116
119
|
|
|
117
120
|
await asyncio.get_event_loop().run_in_executor(self.executor, _checkout)
|
|
118
121
|
|
|
122
|
+
async def restore_to_branch(
|
|
123
|
+
self, local_path: Path, branch_name: str = "main"
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Restore repository to a specific branch, recovering from detached HEAD.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
local_path: Path to the repository
|
|
129
|
+
branch_name: Branch to restore to (default: "main")
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def _restore() -> None:
|
|
134
|
+
try:
|
|
135
|
+
repo = Repo(local_path)
|
|
136
|
+
|
|
137
|
+
# Try to checkout the requested branch
|
|
138
|
+
try:
|
|
139
|
+
repo.git.checkout(branch_name)
|
|
140
|
+
except Exception: # noqa: BLE001
|
|
141
|
+
# If requested branch doesn't exist, try common default branches
|
|
142
|
+
for fallback in ["master", "develop"]:
|
|
143
|
+
try:
|
|
144
|
+
repo.git.checkout(fallback)
|
|
145
|
+
except Exception: # noqa: BLE001
|
|
146
|
+
# Branch doesn't exist, try next fallback
|
|
147
|
+
self._log.debug(f"Branch {fallback} not found, trying next")
|
|
148
|
+
else:
|
|
149
|
+
self._log.debug(
|
|
150
|
+
f"Branch {branch_name} not found, "
|
|
151
|
+
f"restored to {fallback} instead"
|
|
152
|
+
)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
# If all branches fail, stay in detached state
|
|
156
|
+
self._log.warning(
|
|
157
|
+
f"Could not restore to any branch in {local_path}, "
|
|
158
|
+
f"repository remains in detached HEAD state"
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
self._log.debug(f"Restored repository to branch {branch_name}")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
self._log.error(f"Failed to restore branch in {local_path}: {e}")
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
await asyncio.get_event_loop().run_in_executor(self.executor, _restore)
|
|
167
|
+
|
|
119
168
|
async def pull_repository(self, local_path: Path) -> None:
|
|
120
169
|
"""Pull latest changes for existing repository."""
|
|
121
170
|
|
|
@@ -139,12 +188,20 @@ class GitPythonAdapter(GitAdapter):
|
|
|
139
188
|
repo = Repo(local_path)
|
|
140
189
|
|
|
141
190
|
# Get local branches
|
|
191
|
+
# Check if HEAD is detached
|
|
192
|
+
try:
|
|
193
|
+
active_branch = repo.active_branch
|
|
194
|
+
except TypeError:
|
|
195
|
+
# HEAD is detached, no active branch
|
|
196
|
+
active_branch = None
|
|
197
|
+
|
|
142
198
|
branches = [
|
|
143
199
|
{
|
|
144
200
|
"name": branch.name,
|
|
145
201
|
"type": "local",
|
|
146
202
|
"head_commit_sha": branch.commit.hexsha,
|
|
147
|
-
"is_active":
|
|
203
|
+
"is_active": active_branch is not None
|
|
204
|
+
and branch == active_branch,
|
|
148
205
|
}
|
|
149
206
|
for branch in repo.branches
|
|
150
207
|
]
|
|
@@ -291,7 +348,7 @@ class GitPythonAdapter(GitAdapter):
|
|
|
291
348
|
async def get_commit_files(
|
|
292
349
|
self, local_path: Path, commit_sha: str
|
|
293
350
|
) -> list[dict[str, Any]]:
|
|
294
|
-
"""Get all files in a specific commit."""
|
|
351
|
+
"""Get all files in a specific commit from the git tree."""
|
|
295
352
|
|
|
296
353
|
def _get_files() -> list[dict[str, Any]]:
|
|
297
354
|
try:
|
|
@@ -332,6 +389,16 @@ class GitPythonAdapter(GitAdapter):
|
|
|
332
389
|
|
|
333
390
|
return await asyncio.get_event_loop().run_in_executor(self.executor, _get_files)
|
|
334
391
|
|
|
392
|
+
async def get_commit_file_data(
|
|
393
|
+
self, local_path: Path, commit_sha: str
|
|
394
|
+
) -> list[dict[str, Any]]:
|
|
395
|
+
"""Get file metadata for a commit, with files checked out to disk."""
|
|
396
|
+
await self._checkout_commit(local_path, commit_sha)
|
|
397
|
+
try:
|
|
398
|
+
return await self.get_commit_files(local_path, commit_sha)
|
|
399
|
+
finally:
|
|
400
|
+
await self.restore_to_branch(local_path, "main")
|
|
401
|
+
|
|
335
402
|
async def repository_exists(self, local_path: Path) -> bool:
|
|
336
403
|
"""Check if repository exists at local path."""
|
|
337
404
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Generic enricher infrastructure implementations."""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Enricher factory for creating generic enricher domain services."""
|
|
2
|
+
|
|
3
|
+
from kodit.config import AppContext, Endpoint
|
|
4
|
+
from kodit.domain.enrichments.enricher import Enricher
|
|
5
|
+
from kodit.infrastructure.enricher.litellm_enricher import LiteLLMEnricher
|
|
6
|
+
from kodit.infrastructure.enricher.local_enricher import LocalEnricher
|
|
7
|
+
from kodit.infrastructure.enricher.null_enricher import NullEnricher
|
|
8
|
+
from kodit.log import log_event
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_endpoint_configuration(app_context: AppContext) -> Endpoint | None:
|
|
12
|
+
"""Get the endpoint configuration for the enricher service.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
app_context: The application context.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The endpoint configuration or None.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
return app_context.enrichment_endpoint or None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def enricher_domain_service_factory(
|
|
25
|
+
app_context: AppContext,
|
|
26
|
+
*,
|
|
27
|
+
use_null_enricher: bool = False,
|
|
28
|
+
) -> Enricher:
|
|
29
|
+
"""Create an enricher domain service.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
app_context: The application context.
|
|
33
|
+
use_null_enricher: Whether to use the null enricher instead.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
An enricher domain service instance.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
enricher: Enricher
|
|
40
|
+
|
|
41
|
+
if use_null_enricher:
|
|
42
|
+
log_event("kodit.enricher", {"provider": "null"})
|
|
43
|
+
enricher = NullEnricher()
|
|
44
|
+
else:
|
|
45
|
+
endpoint = _get_endpoint_configuration(app_context)
|
|
46
|
+
if endpoint:
|
|
47
|
+
log_event("kodit.enricher", {"provider": "litellm"})
|
|
48
|
+
enricher = LiteLLMEnricher(endpoint=endpoint)
|
|
49
|
+
else:
|
|
50
|
+
log_event("kodit.enricher", {"provider": "local"})
|
|
51
|
+
enricher = LocalEnricher()
|
|
52
|
+
|
|
53
|
+
return enricher
|
kodit/infrastructure/{enrichment/litellm_enrichment_provider.py → enricher/litellm_enricher.py}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""LiteLLM
|
|
1
|
+
"""LiteLLM enricher implementation."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from collections.abc import AsyncGenerator
|
|
@@ -10,27 +10,22 @@ import structlog
|
|
|
10
10
|
from litellm import acompletion
|
|
11
11
|
|
|
12
12
|
from kodit.config import Endpoint
|
|
13
|
-
from kodit.domain.
|
|
14
|
-
from kodit.domain.
|
|
15
|
-
from kodit.
|
|
13
|
+
from kodit.domain.enrichments.enricher import Enricher
|
|
14
|
+
from kodit.domain.enrichments.request import EnrichmentRequest
|
|
15
|
+
from kodit.domain.enrichments.response import EnrichmentResponse
|
|
16
|
+
from kodit.infrastructure.enricher.utils import clean_thinking_tags
|
|
16
17
|
|
|
17
|
-
ENRICHMENT_SYSTEM_PROMPT = """
|
|
18
|
-
You are a professional software developer. You will be given a snippet of code.
|
|
19
|
-
Please provide a concise explanation of the code.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
# Default tuned conservatively for broad provider compatibility
|
|
23
18
|
DEFAULT_NUM_PARALLEL_TASKS = 20
|
|
24
19
|
|
|
25
20
|
|
|
26
|
-
class
|
|
27
|
-
"""LiteLLM
|
|
21
|
+
class LiteLLMEnricher(Enricher):
|
|
22
|
+
"""LiteLLM enricher that supports 100+ providers."""
|
|
28
23
|
|
|
29
24
|
def __init__(
|
|
30
25
|
self,
|
|
31
26
|
endpoint: Endpoint,
|
|
32
27
|
) -> None:
|
|
33
|
-
"""Initialize the LiteLLM
|
|
28
|
+
"""Initialize the LiteLLM enricher.
|
|
34
29
|
|
|
35
30
|
Args:
|
|
36
31
|
endpoint: The endpoint configuration containing all settings.
|
|
@@ -44,23 +39,20 @@ class LiteLLMEnrichmentProvider(EnrichmentProvider):
|
|
|
44
39
|
self.num_parallel_tasks = (
|
|
45
40
|
endpoint.num_parallel_tasks or DEFAULT_NUM_PARALLEL_TASKS
|
|
46
41
|
)
|
|
47
|
-
self.timeout = endpoint.timeout
|
|
42
|
+
self.timeout = endpoint.timeout
|
|
48
43
|
self.extra_params = endpoint.extra_params or {}
|
|
49
44
|
|
|
50
|
-
# Configure LiteLLM with custom HTTPX client for Unix socket support if needed
|
|
51
45
|
self._setup_litellm_client()
|
|
52
46
|
|
|
53
47
|
def _setup_litellm_client(self) -> None:
|
|
54
48
|
"""Set up LiteLLM with custom HTTPX client for Unix socket support."""
|
|
55
49
|
if self.socket_path:
|
|
56
|
-
# Create HTTPX client with Unix socket transport
|
|
57
50
|
transport = httpx.AsyncHTTPTransport(uds=self.socket_path)
|
|
58
51
|
unix_client = httpx.AsyncClient(
|
|
59
52
|
transport=transport,
|
|
60
|
-
base_url="http://localhost",
|
|
53
|
+
base_url="http://localhost",
|
|
61
54
|
timeout=self.timeout,
|
|
62
55
|
)
|
|
63
|
-
# Set as LiteLLM's async client session
|
|
64
56
|
litellm.aclient_session = unix_client
|
|
65
57
|
|
|
66
58
|
async def _call_chat_completion(self, messages: list[dict[str, str]]) -> Any:
|
|
@@ -79,20 +71,17 @@ class LiteLLMEnrichmentProvider(EnrichmentProvider):
|
|
|
79
71
|
"timeout": self.timeout,
|
|
80
72
|
}
|
|
81
73
|
|
|
82
|
-
# Add API key if provided
|
|
83
74
|
if self.api_key:
|
|
84
75
|
kwargs["api_key"] = self.api_key
|
|
85
76
|
|
|
86
|
-
# Add base_url if provided
|
|
87
77
|
if self.base_url:
|
|
88
78
|
kwargs["api_base"] = self.base_url
|
|
89
79
|
|
|
90
|
-
# Add extra parameters
|
|
91
80
|
kwargs.update(self.extra_params)
|
|
92
81
|
|
|
93
82
|
try:
|
|
94
|
-
# Use litellm's async completion function
|
|
95
83
|
response = await acompletion(**kwargs)
|
|
84
|
+
self.log.debug("enrichment request", request=kwargs, response=response)
|
|
96
85
|
return (
|
|
97
86
|
response.model_dump() if hasattr(response, "model_dump") else response
|
|
98
87
|
)
|
|
@@ -108,30 +97,31 @@ class LiteLLMEnrichmentProvider(EnrichmentProvider):
|
|
|
108
97
|
"""Enrich a list of requests using LiteLLM.
|
|
109
98
|
|
|
110
99
|
Args:
|
|
111
|
-
requests: List of enrichment requests.
|
|
100
|
+
requests: List of generic enrichment requests.
|
|
112
101
|
|
|
113
102
|
Yields:
|
|
114
|
-
|
|
103
|
+
Generic enrichment responses as they are processed.
|
|
115
104
|
|
|
116
105
|
"""
|
|
117
106
|
if not requests:
|
|
118
107
|
self.log.warning("No requests for enrichment")
|
|
119
108
|
return
|
|
120
109
|
|
|
121
|
-
# Process requests in parallel with a semaphore to limit concurrent requests
|
|
122
110
|
sem = asyncio.Semaphore(self.num_parallel_tasks)
|
|
123
111
|
|
|
124
|
-
async def process_request(
|
|
112
|
+
async def process_request(
|
|
113
|
+
request: EnrichmentRequest,
|
|
114
|
+
) -> EnrichmentResponse:
|
|
125
115
|
async with sem:
|
|
126
116
|
if not request.text:
|
|
127
117
|
return EnrichmentResponse(
|
|
128
|
-
|
|
118
|
+
id=request.id,
|
|
129
119
|
text="",
|
|
130
120
|
)
|
|
131
121
|
messages = [
|
|
132
122
|
{
|
|
133
123
|
"role": "system",
|
|
134
|
-
"content":
|
|
124
|
+
"content": request.system_prompt,
|
|
135
125
|
},
|
|
136
126
|
{"role": "user", "content": request.text},
|
|
137
127
|
]
|
|
@@ -141,22 +131,19 @@ class LiteLLMEnrichmentProvider(EnrichmentProvider):
|
|
|
141
131
|
.get("message", {})
|
|
142
132
|
.get("content", "")
|
|
143
133
|
)
|
|
144
|
-
# Remove thinking tags from the response
|
|
145
134
|
cleaned_content = clean_thinking_tags(content or "")
|
|
146
135
|
return EnrichmentResponse(
|
|
147
|
-
|
|
136
|
+
id=request.id,
|
|
148
137
|
text=cleaned_content,
|
|
149
138
|
)
|
|
150
139
|
|
|
151
|
-
# Create tasks for all requests
|
|
152
140
|
tasks = [process_request(request) for request in requests]
|
|
153
141
|
|
|
154
|
-
# Process all requests and yield results as they complete
|
|
155
142
|
for task in asyncio.as_completed(tasks):
|
|
156
143
|
yield await task
|
|
157
144
|
|
|
158
145
|
async def close(self) -> None:
|
|
159
|
-
"""Close the
|
|
146
|
+
"""Close the enricher and cleanup HTTPX client if using Unix sockets."""
|
|
160
147
|
if (
|
|
161
148
|
self.socket_path
|
|
162
149
|
and hasattr(litellm, "aclient_session")
|