kodit 0.2.3__tar.gz → 0.2.4__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.

Potentially problematic release.


This version of kodit might be problematic. Click here for more details.

Files changed (155) hide show
  1. {kodit-0.2.3 → kodit-0.2.4}/PKG-INFO +1 -1
  2. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/_version.py +2 -2
  3. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/embedding/embedding_factory.py +6 -0
  4. kodit-0.2.4/src/kodit/embedding/embedding_provider/embedding_provider.py +92 -0
  5. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/embedding/embedding_provider/hash_embedding_provider.py +16 -7
  6. kodit-0.2.4/src/kodit/embedding/embedding_provider/local_embedding_provider.py +96 -0
  7. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/embedding/embedding_provider/openai_embedding_provider.py +18 -22
  8. kodit-0.2.4/src/kodit/embedding/local_vector_search_service.py +87 -0
  9. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/embedding/vector_search_service.py +18 -1
  10. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/embedding/vectorchord_vector_search_service.py +63 -16
  11. kodit-0.2.4/src/kodit/enrichment/enrichment_provider/enrichment_provider.py +36 -0
  12. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/enrichment/enrichment_provider/local_enrichment_provider.py +39 -28
  13. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/enrichment/enrichment_provider/openai_enrichment_provider.py +25 -27
  14. kodit-0.2.4/src/kodit/enrichment/enrichment_service.py +45 -0
  15. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/indexing/indexing_service.py +26 -20
  16. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/embedding/embedding_provider/local_embedding_provider_test.py +48 -11
  17. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/embedding/embedding_provider/openai_embedding_provider_test.py +38 -10
  18. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/embedding/local_vector_search_service_test.py +32 -3
  19. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/embedding/vectorchord_vector_search_service_test.py +31 -5
  20. kodit-0.2.4/tests/kodit/enrichment/enrichment_provider/local_enrichment_provider_test.py +218 -0
  21. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/enrichment/enrichment_provider/openai_enrichment_provider_test.py +78 -47
  22. kodit-0.2.3/src/kodit/embedding/embedding_provider/embedding_provider.py +0 -64
  23. kodit-0.2.3/src/kodit/embedding/embedding_provider/local_embedding_provider.py +0 -64
  24. kodit-0.2.3/src/kodit/embedding/local_vector_search_service.py +0 -54
  25. kodit-0.2.3/src/kodit/enrichment/enrichment_provider/enrichment_provider.py +0 -16
  26. kodit-0.2.3/src/kodit/enrichment/enrichment_service.py +0 -33
  27. {kodit-0.2.3 → kodit-0.2.4}/.cursor/rules/kodit.mdc +0 -0
  28. {kodit-0.2.3 → kodit-0.2.4}/.dockerignore +0 -0
  29. {kodit-0.2.3 → kodit-0.2.4}/.github/CODE_OF_CONDUCT.md +0 -0
  30. {kodit-0.2.3 → kodit-0.2.4}/.github/CONTRIBUTING.md +0 -0
  31. {kodit-0.2.3 → kodit-0.2.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  32. {kodit-0.2.3 → kodit-0.2.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  33. {kodit-0.2.3 → kodit-0.2.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  34. {kodit-0.2.3 → kodit-0.2.4}/.github/dependabot.yml +0 -0
  35. {kodit-0.2.3 → kodit-0.2.4}/.github/workflows/docker.yaml +0 -0
  36. {kodit-0.2.3 → kodit-0.2.4}/.github/workflows/docs.yaml +0 -0
  37. {kodit-0.2.3 → kodit-0.2.4}/.github/workflows/pull_request.yaml +0 -0
  38. {kodit-0.2.3 → kodit-0.2.4}/.github/workflows/pypi-test.yaml +0 -0
  39. {kodit-0.2.3 → kodit-0.2.4}/.github/workflows/pypi.yaml +0 -0
  40. {kodit-0.2.3 → kodit-0.2.4}/.github/workflows/test.yaml +0 -0
  41. {kodit-0.2.3 → kodit-0.2.4}/.gitignore +0 -0
  42. {kodit-0.2.3 → kodit-0.2.4}/.python-version +0 -0
  43. {kodit-0.2.3 → kodit-0.2.4}/.vscode/launch.json +0 -0
  44. {kodit-0.2.3 → kodit-0.2.4}/.vscode/settings.json +0 -0
  45. {kodit-0.2.3 → kodit-0.2.4}/Dockerfile +0 -0
  46. {kodit-0.2.3 → kodit-0.2.4}/LICENSE +0 -0
  47. {kodit-0.2.3 → kodit-0.2.4}/README.md +0 -0
  48. {kodit-0.2.3 → kodit-0.2.4}/alembic.ini +0 -0
  49. {kodit-0.2.3 → kodit-0.2.4}/docs/_index.md +0 -0
  50. {kodit-0.2.3 → kodit-0.2.4}/docs/demos/_index.md +0 -0
  51. {kodit-0.2.3 → kodit-0.2.4}/docs/demos/go-simple-microservice/index.md +0 -0
  52. {kodit-0.2.3 → kodit-0.2.4}/docs/demos/knock-knock-auth/index.md +0 -0
  53. {kodit-0.2.3 → kodit-0.2.4}/docs/developer/index.md +0 -0
  54. {kodit-0.2.3 → kodit-0.2.4}/docs/getting-started/_index.md +0 -0
  55. {kodit-0.2.3 → kodit-0.2.4}/docs/getting-started/installation/index.md +0 -0
  56. {kodit-0.2.3 → kodit-0.2.4}/docs/getting-started/integration/index.md +0 -0
  57. {kodit-0.2.3 → kodit-0.2.4}/docs/getting-started/quick-start/index.md +0 -0
  58. {kodit-0.2.3 → kodit-0.2.4}/docs/reference/_index.md +0 -0
  59. {kodit-0.2.3 → kodit-0.2.4}/docs/reference/configuration/index.md +0 -0
  60. {kodit-0.2.3 → kodit-0.2.4}/docs/reference/deployment/docker-compose.yaml +0 -0
  61. {kodit-0.2.3 → kodit-0.2.4}/docs/reference/deployment/index.md +0 -0
  62. {kodit-0.2.3 → kodit-0.2.4}/docs/reference/deployment/kubernetes.yaml +0 -0
  63. {kodit-0.2.3 → kodit-0.2.4}/docs/reference/telemetry/index.md +0 -0
  64. {kodit-0.2.3 → kodit-0.2.4}/pyproject.toml +0 -0
  65. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/.gitignore +0 -0
  66. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/__init__.py +0 -0
  67. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/app.py +0 -0
  68. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/bm25/__init__.py +0 -0
  69. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/bm25/keyword_search_factory.py +0 -0
  70. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/bm25/keyword_search_service.py +0 -0
  71. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/bm25/local_bm25.py +0 -0
  72. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/bm25/vectorchord_bm25.py +0 -0
  73. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/cli.py +0 -0
  74. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/config.py +0 -0
  75. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/database.py +0 -0
  76. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/embedding/__init__.py +0 -0
  77. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/embedding/embedding_models.py +0 -0
  78. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/embedding/embedding_provider/__init__.py +0 -0
  79. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/embedding/embedding_repository.py +0 -0
  80. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/enrichment/__init__.py +0 -0
  81. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/enrichment/enrichment_factory.py +0 -0
  82. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/enrichment/enrichment_provider/__init__.py +0 -0
  83. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/indexing/__init__.py +0 -0
  84. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/indexing/fusion.py +0 -0
  85. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/indexing/indexing_models.py +0 -0
  86. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/indexing/indexing_repository.py +0 -0
  87. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/log.py +0 -0
  88. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/mcp.py +0 -0
  89. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/middleware.py +0 -0
  90. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/migrations/README +0 -0
  91. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/migrations/__init__.py +0 -0
  92. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/migrations/env.py +0 -0
  93. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/migrations/script.py.mako +0 -0
  94. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/migrations/versions/7c3bbc2ab32b_add_embeddings_table.py +0 -0
  95. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/migrations/versions/85155663351e_initial.py +0 -0
  96. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/migrations/versions/9e53ea8bb3b0_add_authors.py +0 -0
  97. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/migrations/versions/__init__.py +0 -0
  98. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/migrations/versions/c3f5137d30f5_index_all_the_things.py +0 -0
  99. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/snippets/__init__.py +0 -0
  100. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/snippets/languages/__init__.py +0 -0
  101. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/snippets/languages/csharp.scm +0 -0
  102. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/snippets/languages/go.scm +0 -0
  103. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/snippets/languages/javascript.scm +0 -0
  104. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/snippets/languages/python.scm +0 -0
  105. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/snippets/languages/typescript.scm +0 -0
  106. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/snippets/method_snippets.py +0 -0
  107. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/snippets/snippets.py +0 -0
  108. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/source/__init__.py +0 -0
  109. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/source/git.py +0 -0
  110. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/source/ignore.py +0 -0
  111. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/source/source_factories.py +0 -0
  112. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/source/source_models.py +0 -0
  113. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/source/source_repository.py +0 -0
  114. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/source/source_service.py +0 -0
  115. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/util/__init__.py +0 -0
  116. {kodit-0.2.3 → kodit-0.2.4}/src/kodit/util/spinner.py +0 -0
  117. {kodit-0.2.3 → kodit-0.2.4}/tests/__init__.py +0 -0
  118. {kodit-0.2.3 → kodit-0.2.4}/tests/conftest.py +0 -0
  119. {kodit-0.2.3 → kodit-0.2.4}/tests/docker-smoke.sh +0 -0
  120. {kodit-0.2.3 → kodit-0.2.4}/tests/experiments/cline-prompt-regression-tests/cline_prompt.txt +0 -0
  121. {kodit-0.2.3 → kodit-0.2.4}/tests/experiments/cline-prompt-regression-tests/cline_prompt_test.py +0 -0
  122. {kodit-0.2.3 → kodit-0.2.4}/tests/experiments/embedding.py +0 -0
  123. {kodit-0.2.3 → kodit-0.2.4}/tests/experiments/similarity_test.py +0 -0
  124. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/__init__.py +0 -0
  125. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/bm25/local_bm25_test.py +0 -0
  126. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/bm25/vectorchord_repository_test.py +0 -0
  127. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/cli_test.py +0 -0
  128. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/e2e.py +0 -0
  129. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/embedding/__init__.py +0 -0
  130. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/embedding/embedding_factory_test.py +0 -0
  131. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/enrichment/__init__.py +0 -0
  132. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/enrichment/enrichment_factory_test.py +0 -0
  133. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/enrichment/enrichment_provider/__init__.py +0 -0
  134. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/indexing/__init__.py +0 -0
  135. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/indexing/indexing_repository_test.py +0 -0
  136. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/indexing/indexing_service_test.py +0 -0
  137. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/log_test.py +0 -0
  138. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/mcp_test.py +0 -0
  139. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/snippets/__init__.py +0 -0
  140. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/snippets/csharp.cs +0 -0
  141. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/snippets/detect_language_test.py +0 -0
  142. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/snippets/golang.go +0 -0
  143. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/snippets/javascript.js +0 -0
  144. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/snippets/knock-knock-server.py +0 -0
  145. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/snippets/method_extraction_test.py +0 -0
  146. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/snippets/python.py +0 -0
  147. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/snippets/typescript.tsx +0 -0
  148. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/source/__init__.py +0 -0
  149. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/source/git_test.py +0 -0
  150. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/source/ignore_test.py +0 -0
  151. {kodit-0.2.3 → kodit-0.2.4}/tests/kodit/source/source_service_test.py +0 -0
  152. {kodit-0.2.3 → kodit-0.2.4}/tests/performance/similarity.py +0 -0
  153. {kodit-0.2.3 → kodit-0.2.4}/tests/smoke.sh +0 -0
  154. {kodit-0.2.3 → kodit-0.2.4}/tests/vectorchord-smoke.sh +0 -0
  155. {kodit-0.2.3 → kodit-0.2.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kodit
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Code indexing for better AI code generation
5
5
  Project-URL: Homepage, https://docs.helixml.tech/kodit/
6
6
  Project-URL: Documentation, https://docs.helixml.tech/kodit/
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.2.3'
21
- __version_tuple__ = version_tuple = (0, 2, 3)
20
+ __version__ = version = '0.2.4'
21
+ __version_tuple__ = version_tuple = (0, 2, 4)
@@ -3,6 +3,7 @@
3
3
  from sqlalchemy.ext.asyncio import AsyncSession
4
4
 
5
5
  from kodit.config import AppContext, Endpoint
6
+ from kodit.embedding.embedding_models import EmbeddingType
6
7
  from kodit.embedding.embedding_provider.local_embedding_provider import (
7
8
  CODE,
8
9
  LocalEmbeddingProvider,
@@ -54,9 +55,14 @@ def embedding_factory(
54
55
  return VectorChordVectorSearchService(task_name, session, embedding_provider)
55
56
  if app_context.default_search.provider == "sqlite":
56
57
  log_event("kodit.database", {"provider": "sqlite"})
58
+ if task_name == "code":
59
+ embedding_type = EmbeddingType.CODE
60
+ elif task_name == "text":
61
+ embedding_type = EmbeddingType.TEXT
57
62
  return LocalVectorSearchService(
58
63
  embedding_repository=embedding_repository,
59
64
  embedding_provider=embedding_provider,
65
+ embedding_type=embedding_type,
60
66
  )
61
67
 
62
68
  msg = f"Invalid semantic search provider: {app_context.default_search.provider}"
@@ -0,0 +1,92 @@
1
+ """Embedding provider."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import AsyncGenerator
5
+ from dataclasses import dataclass
6
+
7
+ import structlog
8
+ import tiktoken
9
+
10
+ OPENAI_MAX_EMBEDDING_SIZE = 8192
11
+
12
+ Vector = list[float]
13
+
14
+
15
+ @dataclass
16
+ class EmbeddingRequest:
17
+ """Embedding request."""
18
+
19
+ id: int
20
+ text: str
21
+
22
+
23
+ @dataclass
24
+ class EmbeddingResponse:
25
+ """Embedding response."""
26
+
27
+ id: int
28
+ embedding: Vector
29
+
30
+
31
+ class EmbeddingProvider(ABC):
32
+ """Embedding provider."""
33
+
34
+ @abstractmethod
35
+ def embed(
36
+ self, data: list[EmbeddingRequest]
37
+ ) -> AsyncGenerator[list[EmbeddingResponse], None]:
38
+ """Embed a list of strings.
39
+
40
+ The embedding provider is responsible for embedding a list of strings into a
41
+ list of vectors. The embedding provider is responsible for splitting the list of
42
+ strings into smaller sub-batches and embedding them in parallel.
43
+ """
44
+
45
+
46
+ def split_sub_batches(
47
+ encoding: tiktoken.Encoding,
48
+ data: list[EmbeddingRequest],
49
+ max_context_window: int = OPENAI_MAX_EMBEDDING_SIZE,
50
+ ) -> list[list[EmbeddingRequest]]:
51
+ """Split a list of strings into smaller sub-batches."""
52
+ log = structlog.get_logger(__name__)
53
+ result = []
54
+ data_to_process = [s for s in data if s.text.strip()] # Filter out empty strings
55
+
56
+ while data_to_process:
57
+ next_batch = []
58
+ current_tokens = 0
59
+
60
+ while data_to_process:
61
+ next_item = data_to_process[0]
62
+ item_tokens = len(encoding.encode(next_item.text, disallowed_special=()))
63
+
64
+ if item_tokens > max_context_window:
65
+ # Optimise truncation by operating on tokens directly instead of
66
+ # removing one character at a time and repeatedly re-encoding.
67
+ tokens = encoding.encode(next_item.text, disallowed_special=())
68
+ if len(tokens) > max_context_window:
69
+ # Keep only the first *max_context_window* tokens.
70
+ tokens = tokens[:max_context_window]
71
+ # Convert back to text. This requires only one decode call and
72
+ # guarantees that the resulting string fits the token budget.
73
+ next_item.text = encoding.decode(tokens)
74
+ item_tokens = max_context_window # We know the exact size now
75
+
76
+ data_to_process[0] = next_item
77
+
78
+ log.warning(
79
+ "Truncated snippet because it was too long to embed",
80
+ snippet=next_item.text[:100] + "...",
81
+ )
82
+
83
+ if current_tokens + item_tokens > max_context_window:
84
+ break
85
+
86
+ next_batch.append(data_to_process.pop(0))
87
+ current_tokens += item_tokens
88
+
89
+ if next_batch:
90
+ result.append(next_batch)
91
+
92
+ return result
@@ -3,10 +3,12 @@
3
3
  import asyncio
4
4
  import hashlib
5
5
  import math
6
- from collections.abc import Generator, Sequence
6
+ from collections.abc import AsyncGenerator, Generator, Sequence
7
7
 
8
8
  from kodit.embedding.embedding_provider.embedding_provider import (
9
9
  EmbeddingProvider,
10
+ EmbeddingRequest,
11
+ EmbeddingResponse,
10
12
  Vector,
11
13
  )
12
14
 
@@ -31,27 +33,34 @@ class HashEmbeddingProvider(EmbeddingProvider):
31
33
  self.dim = dim
32
34
  self.batch_size = batch_size
33
35
 
34
- async def embed(self, data: list[str]) -> list[Vector]:
36
+ async def embed(
37
+ self, data: list[EmbeddingRequest]
38
+ ) -> AsyncGenerator[list[EmbeddingResponse], None]:
35
39
  """Embed every string in *data*, preserving order.
36
40
 
37
41
  Work is sliced into *batch_size* chunks and scheduled concurrently
38
42
  (still CPU-bound, but enough to cooperate with an asyncio loop).
39
43
  """
40
44
  if not data:
41
- return []
45
+ yield []
42
46
 
43
47
  async def _embed_chunk(chunk: Sequence[str]) -> list[Vector]:
44
48
  return [self._string_to_vector(text) for text in chunk]
45
49
 
46
50
  tasks = [
47
51
  asyncio.create_task(_embed_chunk(chunk))
48
- for chunk in self._chunked(data, self.batch_size)
52
+ for chunk in self._chunked([i.text for i in data], self.batch_size)
49
53
  ]
50
54
 
51
- vectors: list[Vector] = []
52
55
  for task in tasks:
53
- vectors.extend(await task)
54
- return vectors
56
+ result = await task
57
+ yield [
58
+ EmbeddingResponse(
59
+ id=item.id,
60
+ embedding=embedding,
61
+ )
62
+ for item, embedding in zip(data, result, strict=True)
63
+ ]
55
64
 
56
65
  @staticmethod
57
66
  def _chunked(seq: Sequence[str], size: int) -> Generator[Sequence[str], None, None]:
@@ -0,0 +1,96 @@
1
+ """Local embedding service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from time import time
7
+ from typing import TYPE_CHECKING
8
+
9
+ import structlog
10
+
11
+ from kodit.embedding.embedding_provider.embedding_provider import (
12
+ EmbeddingProvider,
13
+ EmbeddingRequest,
14
+ EmbeddingResponse,
15
+ split_sub_batches,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import AsyncGenerator
20
+
21
+ from sentence_transformers import SentenceTransformer
22
+ from tiktoken import Encoding
23
+
24
+
25
+ TINY = "tiny"
26
+ CODE = "code"
27
+ TEST = "test"
28
+
29
+ COMMON_EMBEDDING_MODELS = {
30
+ TINY: "ibm-granite/granite-embedding-30m-english",
31
+ CODE: "flax-sentence-embeddings/st-codesearch-distilroberta-base",
32
+ TEST: "minishlab/potion-base-4M",
33
+ }
34
+
35
+
36
+ class LocalEmbeddingProvider(EmbeddingProvider):
37
+ """Local embedder."""
38
+
39
+ def __init__(self, model_name: str) -> None:
40
+ """Initialize the local embedder."""
41
+ self.log = structlog.get_logger(__name__)
42
+ self.model_name = COMMON_EMBEDDING_MODELS.get(model_name, model_name)
43
+ self.encoding_name = "text-embedding-3-small"
44
+ self.embedding_model = None
45
+ self.encoding = None
46
+
47
+ def _encoding(self) -> Encoding:
48
+ if self.encoding is None:
49
+ from tiktoken import encoding_for_model
50
+
51
+ start_time = time()
52
+ self.encoding = encoding_for_model(self.encoding_name)
53
+ self.log.debug(
54
+ "Encoding loaded",
55
+ model_name=self.encoding_name,
56
+ duration=time() - start_time,
57
+ )
58
+ return self.encoding
59
+
60
+ def _model(self) -> SentenceTransformer:
61
+ """Get the embedding model."""
62
+ if self.embedding_model is None:
63
+ os.environ["TOKENIZERS_PARALLELISM"] = "false" # Avoid warnings
64
+ from sentence_transformers import SentenceTransformer
65
+
66
+ start_time = time()
67
+ self.embedding_model = SentenceTransformer(
68
+ self.model_name,
69
+ trust_remote_code=True,
70
+ )
71
+ self.log.debug(
72
+ "Model loaded",
73
+ model_name=self.model_name,
74
+ duration=time() - start_time,
75
+ )
76
+ return self.embedding_model
77
+
78
+ async def embed(
79
+ self, data: list[EmbeddingRequest]
80
+ ) -> AsyncGenerator[list[EmbeddingResponse], None]:
81
+ """Embed a list of strings."""
82
+ model = self._model()
83
+
84
+ batched_data = split_sub_batches(self._encoding(), data)
85
+
86
+ for batch in batched_data:
87
+ embeddings = model.encode(
88
+ [i.text for i in batch], show_progress_bar=False, batch_size=4
89
+ )
90
+ yield [
91
+ EmbeddingResponse(
92
+ id=item.id,
93
+ embedding=[float(x) for x in embedding],
94
+ )
95
+ for item, embedding in zip(batch, embeddings, strict=True)
96
+ ]
@@ -1,6 +1,7 @@
1
1
  """OpenAI embedding service."""
2
2
 
3
3
  import asyncio
4
+ from collections.abc import AsyncGenerator
4
5
 
5
6
  import structlog
6
7
  import tiktoken
@@ -8,7 +9,8 @@ from openai import AsyncOpenAI
8
9
 
9
10
  from kodit.embedding.embedding_provider.embedding_provider import (
10
11
  EmbeddingProvider,
11
- Vector,
12
+ EmbeddingRequest,
13
+ EmbeddingResponse,
12
14
  split_sub_batches,
13
15
  )
14
16
 
@@ -31,7 +33,9 @@ class OpenAIEmbeddingProvider(EmbeddingProvider):
31
33
  "text-embedding-3-small"
32
34
  ) # Sensible default
33
35
 
34
- async def embed(self, data: list[str]) -> list[Vector]:
36
+ async def embed(
37
+ self, data: list[EmbeddingRequest]
38
+ ) -> AsyncGenerator[list[EmbeddingResponse], None]:
35
39
  """Embed a list of documents."""
36
40
  # First split the list into a list of list where each sublist has fewer than
37
41
  # max tokens.
@@ -40,38 +44,30 @@ class OpenAIEmbeddingProvider(EmbeddingProvider):
40
44
  # Process batches in parallel with a semaphore to limit concurrent requests
41
45
  sem = asyncio.Semaphore(OPENAI_NUM_PARALLEL_TASKS)
42
46
 
43
- # Create a list of tuples with a temporary id for each batch
44
- # We need to do this so that we can return the results in the same order as the
45
- # input data
46
- input_data = [(i, batch) for i, batch in enumerate(batched_data)]
47
-
48
47
  async def process_batch(
49
- data: tuple[int, list[str]],
50
- ) -> tuple[int, list[Vector]]:
51
- batch_id, batch = data
48
+ data: list[EmbeddingRequest],
49
+ ) -> list[EmbeddingResponse]:
52
50
  async with sem:
53
51
  try:
54
52
  response = await self.openai_client.embeddings.create(
55
53
  model=self.model_name,
56
- input=batch,
54
+ input=[i.text for i in data],
57
55
  )
58
- return batch_id, [
59
- [float(x) for x in embedding.embedding]
60
- for embedding in response.data
56
+ return [
57
+ EmbeddingResponse(
58
+ id=item.id,
59
+ embedding=embedding.embedding,
60
+ )
61
+ for item, embedding in zip(data, response.data, strict=True)
61
62
  ]
62
63
  except Exception as e:
63
64
  self.log.exception("Error embedding batch", error=str(e))
64
- return batch_id, []
65
+ return []
65
66
 
66
67
  # Create tasks for all batches
67
- tasks = [process_batch(batch) for batch in input_data]
68
+ tasks = [process_batch(batch) for batch in batched_data]
68
69
 
69
70
  # Process all batches and yield results as they complete
70
- results: list[tuple[int, list[Vector]]] = []
71
71
  for task in asyncio.as_completed(tasks):
72
72
  result = await task
73
- results.append(result)
74
-
75
- # Output in the same order as the input data
76
- ordered_results = [result for _, result in sorted(results, key=lambda x: x[0])]
77
- return [item for sublist in ordered_results for item in sublist]
73
+ yield result
@@ -0,0 +1,87 @@
1
+ """Local vector search."""
2
+
3
+ from collections.abc import AsyncGenerator
4
+
5
+ import structlog
6
+ import tiktoken
7
+
8
+ from kodit.embedding.embedding_models import Embedding, EmbeddingType
9
+ from kodit.embedding.embedding_provider.embedding_provider import (
10
+ EmbeddingProvider,
11
+ EmbeddingRequest,
12
+ )
13
+ from kodit.embedding.embedding_repository import EmbeddingRepository
14
+ from kodit.embedding.vector_search_service import (
15
+ IndexResult,
16
+ VectorSearchRequest,
17
+ VectorSearchResponse,
18
+ VectorSearchService,
19
+ )
20
+
21
+
22
+ class LocalVectorSearchService(VectorSearchService):
23
+ """Local vector search."""
24
+
25
+ def __init__(
26
+ self,
27
+ embedding_repository: EmbeddingRepository,
28
+ embedding_provider: EmbeddingProvider,
29
+ embedding_type: EmbeddingType = EmbeddingType.CODE,
30
+ ) -> None:
31
+ """Initialize the local embedder."""
32
+ self.log = structlog.get_logger(__name__)
33
+ self.embedding_repository = embedding_repository
34
+ self.embedding_provider = embedding_provider
35
+ self.encoding = tiktoken.encoding_for_model("text-embedding-3-small")
36
+ self.embedding_type = embedding_type
37
+
38
+ async def index(
39
+ self, data: list[VectorSearchRequest]
40
+ ) -> AsyncGenerator[list[IndexResult], None]:
41
+ """Embed a list of documents."""
42
+ if not data or len(data) == 0:
43
+ return
44
+
45
+ requests = [EmbeddingRequest(id=doc.snippet_id, text=doc.text) for doc in data]
46
+
47
+ async for batch in self.embedding_provider.embed(requests):
48
+ for result in batch:
49
+ await self.embedding_repository.create_embedding(
50
+ Embedding(
51
+ snippet_id=result.id,
52
+ embedding=result.embedding,
53
+ type=self.embedding_type,
54
+ )
55
+ )
56
+ yield [IndexResult(snippet_id=result.id)]
57
+
58
+ async def retrieve(self, query: str, top_k: int = 10) -> list[VectorSearchResponse]:
59
+ """Query the embedding model."""
60
+ # Build a single-item request and collect its embedding.
61
+ req = EmbeddingRequest(id=0, text=query)
62
+ embedding_vec: list[float] | None = None
63
+ async for batch in self.embedding_provider.embed([req]):
64
+ if batch:
65
+ embedding_vec = [float(v) for v in batch[0].embedding]
66
+ break
67
+
68
+ if not embedding_vec:
69
+ return []
70
+
71
+ results = await self.embedding_repository.list_semantic_results(
72
+ self.embedding_type, embedding_vec, top_k
73
+ )
74
+ return [
75
+ VectorSearchResponse(snippet_id, score) for snippet_id, score in results
76
+ ]
77
+
78
+ async def has_embedding(
79
+ self, snippet_id: int, embedding_type: EmbeddingType
80
+ ) -> bool:
81
+ """Check if a snippet has an embedding."""
82
+ return (
83
+ await self.embedding_repository.get_embedding_by_snippet_id_and_type(
84
+ snippet_id, embedding_type
85
+ )
86
+ is not None
87
+ )
@@ -1,8 +1,11 @@
1
1
  """Embedding service."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
+ from collections.abc import AsyncGenerator
4
5
  from typing import NamedTuple
5
6
 
7
+ from kodit.embedding.embedding_models import EmbeddingType
8
+
6
9
 
7
10
  class VectorSearchResponse(NamedTuple):
8
11
  """Embedding result."""
@@ -18,11 +21,19 @@ class VectorSearchRequest(NamedTuple):
18
21
  text: str
19
22
 
20
23
 
24
+ class IndexResult(NamedTuple):
25
+ """Result of indexing."""
26
+
27
+ snippet_id: int
28
+
29
+
21
30
  class VectorSearchService(ABC):
22
31
  """Semantic search service interface."""
23
32
 
24
33
  @abstractmethod
25
- async def index(self, data: list[VectorSearchRequest]) -> None:
34
+ def index(
35
+ self, data: list[VectorSearchRequest]
36
+ ) -> AsyncGenerator[list[IndexResult], None]:
26
37
  """Embed a list of documents.
27
38
 
28
39
  The embedding service accepts a massive list of id,strings to embed. Behind the
@@ -36,3 +47,9 @@ class VectorSearchService(ABC):
36
47
  @abstractmethod
37
48
  async def retrieve(self, query: str, top_k: int = 10) -> list[VectorSearchResponse]:
38
49
  """Query the embedding model."""
50
+
51
+ @abstractmethod
52
+ async def has_embedding(
53
+ self, snippet_id: int, embedding_type: EmbeddingType
54
+ ) -> bool:
55
+ """Check if a snippet has an embedding."""
@@ -1,13 +1,19 @@
1
1
  """Vectorchord vector search."""
2
2
 
3
+ from collections.abc import AsyncGenerator
3
4
  from typing import Any, Literal
4
5
 
5
6
  import structlog
6
7
  from sqlalchemy import Result, TextClause, text
7
8
  from sqlalchemy.ext.asyncio import AsyncSession
8
9
 
9
- from kodit.embedding.embedding_provider.embedding_provider import EmbeddingProvider
10
+ from kodit.embedding.embedding_models import EmbeddingType
11
+ from kodit.embedding.embedding_provider.embedding_provider import (
12
+ EmbeddingProvider,
13
+ EmbeddingRequest,
14
+ )
10
15
  from kodit.embedding.vector_search_service import (
16
+ IndexResult,
11
17
  VectorSearchRequest,
12
18
  VectorSearchResponse,
13
19
  VectorSearchService,
@@ -52,6 +58,10 @@ ORDER BY score ASC
52
58
  LIMIT :top_k;
53
59
  """
54
60
 
61
+ CHECK_VCHORD_EMBEDDING_EXISTS = """
62
+ SELECT EXISTS(SELECT 1 FROM {TABLE_NAME} WHERE snippet_id = :snippet_id)
63
+ """
64
+
55
65
  TaskName = Literal["code", "text"]
56
66
 
57
67
 
@@ -89,7 +99,15 @@ class VectorChordVectorSearchService(VectorSearchService):
89
99
 
90
100
  async def _create_tables(self) -> None:
91
101
  """Create the necessary tables."""
92
- vector_dim = (await self.embedding_provider.embed(["dimension"]))[0]
102
+ req = EmbeddingRequest(id=0, text="dimension")
103
+ vector_dim: list[float] | None = None
104
+ async for batch in self.embedding_provider.embed([req]):
105
+ if batch:
106
+ vector_dim = batch[0].embedding
107
+ break
108
+ if vector_dim is None:
109
+ msg = "Failed to obtain embedding dimension from provider"
110
+ raise RuntimeError(msg)
93
111
  await self._session.execute(
94
112
  text(
95
113
  f"""CREATE TABLE IF NOT EXISTS {self.table_name} (
@@ -130,31 +148,48 @@ class VectorChordVectorSearchService(VectorSearchService):
130
148
  """Commit the session."""
131
149
  await self._session.commit()
132
150
 
133
- async def index(self, data: list[VectorSearchRequest]) -> None:
151
+ async def index(
152
+ self, data: list[VectorSearchRequest]
153
+ ) -> AsyncGenerator[list[IndexResult], None]:
134
154
  """Embed a list of documents."""
135
155
  if not data or len(data) == 0:
136
156
  self.log.warning("Embedding data is empty, skipping embedding")
137
157
  return
138
158
 
139
- embeddings = await self.embedding_provider.embed([doc.text for doc in data])
140
- # Execute inserts
141
- await self._execute(
142
- text(INSERT_QUERY.format(TABLE_NAME=self.table_name)),
143
- [
144
- {"snippet_id": doc.snippet_id, "embedding": str(embedding)}
145
- for doc, embedding in zip(data, embeddings, strict=True)
146
- ],
147
- )
148
- await self._commit()
159
+ requests = [EmbeddingRequest(id=doc.snippet_id, text=doc.text) for doc in data]
160
+
161
+ async for batch in self.embedding_provider.embed(requests):
162
+ await self._execute(
163
+ text(INSERT_QUERY.format(TABLE_NAME=self.table_name)),
164
+ [
165
+ {
166
+ "snippet_id": result.id,
167
+ "embedding": str(result.embedding),
168
+ }
169
+ for result in batch
170
+ ],
171
+ )
172
+ await self._commit()
173
+ yield [IndexResult(snippet_id=result.id) for result in batch]
149
174
 
150
175
  async def retrieve(self, query: str, top_k: int = 10) -> list[VectorSearchResponse]:
151
176
  """Query the embedding model."""
152
- embedding = await self.embedding_provider.embed([query])
153
- if len(embedding) == 0 or len(embedding[0]) == 0:
177
+ from kodit.embedding.embedding_provider.embedding_provider import (
178
+ EmbeddingRequest,
179
+ )
180
+
181
+ req = EmbeddingRequest(id=0, text=query)
182
+ embedding_vec: list[float] | None = None
183
+ async for batch in self.embedding_provider.embed([req]):
184
+ if batch:
185
+ embedding_vec = batch[0].embedding
186
+ break
187
+
188
+ if not embedding_vec:
154
189
  return []
155
190
  result = await self._execute(
156
191
  text(SEARCH_QUERY.format(TABLE_NAME=self.table_name)),
157
- {"query": str(embedding[0]), "top_k": top_k},
192
+ {"query": str(embedding_vec), "top_k": top_k},
158
193
  )
159
194
  rows = result.mappings().all()
160
195
 
@@ -162,3 +197,15 @@ class VectorChordVectorSearchService(VectorSearchService):
162
197
  VectorSearchResponse(snippet_id=row["snippet_id"], score=row["score"])
163
198
  for row in rows
164
199
  ]
200
+
201
+ async def has_embedding(
202
+ self,
203
+ snippet_id: int,
204
+ embedding_type: EmbeddingType, # noqa: ARG002
205
+ ) -> bool:
206
+ """Check if a snippet has an embedding."""
207
+ result = await self._execute(
208
+ text(CHECK_VCHORD_EMBEDDING_EXISTS.format(TABLE_NAME=self.table_name)),
209
+ {"snippet_id": snippet_id},
210
+ )
211
+ return result.scalar_one()
@@ -0,0 +1,36 @@
1
+ """Enrichment provider."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import AsyncGenerator
5
+ from dataclasses import dataclass
6
+
7
+ ENRICHMENT_SYSTEM_PROMPT = """
8
+ You are a professional software developer. You will be given a snippet of code.
9
+ Please provide a concise explanation of the code.
10
+ """
11
+
12
+
13
+ @dataclass
14
+ class EnrichmentRequest:
15
+ """Enrichment request."""
16
+
17
+ snippet_id: int
18
+ text: str
19
+
20
+
21
+ @dataclass
22
+ class EnrichmentResponse:
23
+ """Enrichment response."""
24
+
25
+ snippet_id: int
26
+ text: str
27
+
28
+
29
+ class EnrichmentProvider(ABC):
30
+ """Enrichment provider."""
31
+
32
+ @abstractmethod
33
+ def enrich(
34
+ self, data: list[EnrichmentRequest]
35
+ ) -> AsyncGenerator[EnrichmentResponse, None]:
36
+ """Enrich a list of strings."""