clanka 0.1.22 → 0.2.1
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.
- package/dist/Agent.d.ts +1 -0
- package/dist/Agent.d.ts.map +1 -1
- package/dist/Agent.js +5 -1
- package/dist/Agent.js.map +1 -1
- package/dist/AgentTools.d.ts.map +1 -1
- package/dist/AgentTools.js +15 -6
- package/dist/AgentTools.js.map +1 -1
- package/dist/ChunkRepo.d.ts +1 -0
- package/dist/ChunkRepo.d.ts.map +1 -1
- package/dist/ChunkRepo.js +1 -0
- package/dist/ChunkRepo.js.map +1 -1
- package/dist/CodeChunker.d.ts +12 -0
- package/dist/CodeChunker.d.ts.map +1 -1
- package/dist/CodeChunker.js +26 -4
- package/dist/CodeChunker.js.map +1 -1
- package/dist/OutputFormatter.js +1 -1
- package/dist/OutputFormatter.js.map +1 -1
- package/dist/SemanticSearch.d.ts +9 -3
- package/dist/SemanticSearch.d.ts.map +1 -1
- package/dist/SemanticSearch.js +99 -36
- package/dist/SemanticSearch.js.map +1 -1
- package/package.json +1 -1
- package/src/Agent.ts +5 -1
- package/src/AgentTools.ts +17 -8
- package/src/ChunkRepo.ts +5 -0
- package/src/CodeChunker.ts +60 -16
- package/src/OutputFormatter.ts +1 -1
- package/src/SemanticSearch.ts +153 -58
package/src/SemanticSearch.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { pipe } from "effect/Function"
|
|
|
10
10
|
import * as EmbeddingModel from "effect/unstable/ai/EmbeddingModel"
|
|
11
11
|
import * as RequestResolver from "effect/RequestResolver"
|
|
12
12
|
import * as Option from "effect/Option"
|
|
13
|
-
import
|
|
13
|
+
import * as Path from "effect/Path"
|
|
14
14
|
import * as ServiceMap from "effect/ServiceMap"
|
|
15
15
|
import * as Fiber from "effect/Fiber"
|
|
16
16
|
import * as Duration from "effect/Duration"
|
|
@@ -23,6 +23,13 @@ import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessS
|
|
|
23
23
|
import type * as FileSystem from "effect/FileSystem"
|
|
24
24
|
import * as Console from "effect/Console"
|
|
25
25
|
|
|
26
|
+
const normalizePath = (path: string) => path.replace(/\\/g, "/")
|
|
27
|
+
|
|
28
|
+
const chunkConfig = {
|
|
29
|
+
chunkSize: 20,
|
|
30
|
+
chunkOverlap: 5,
|
|
31
|
+
} as const
|
|
32
|
+
|
|
26
33
|
/**
|
|
27
34
|
* @since 1.0.0
|
|
28
35
|
* @category Services
|
|
@@ -34,7 +41,8 @@ export class SemanticSearch extends ServiceMap.Service<
|
|
|
34
41
|
readonly query: string
|
|
35
42
|
readonly limit: number
|
|
36
43
|
}): Effect.Effect<string>
|
|
37
|
-
|
|
44
|
+
updateFile(path: string): Effect.Effect<void>
|
|
45
|
+
removeFile(path: string): Effect.Effect<void>
|
|
38
46
|
}
|
|
39
47
|
>()("clanka/SemanticSearch/SemanticSearch") {}
|
|
40
48
|
|
|
@@ -65,73 +73,106 @@ export const layer = (options: {
|
|
|
65
73
|
const chunker = yield* CodeChunker.CodeChunker
|
|
66
74
|
const repo = yield* ChunkRepo.ChunkRepo
|
|
67
75
|
const embeddings = yield* EmbeddingModel.EmbeddingModel
|
|
76
|
+
const pathService = yield* Path.Path
|
|
77
|
+
const root = pathService.resolve(options.directory)
|
|
68
78
|
const resolver = embeddings.resolver.pipe(
|
|
69
79
|
RequestResolver.setDelay(
|
|
70
80
|
options.embeddingBatchSize ?? Duration.millis(50),
|
|
71
81
|
),
|
|
72
82
|
RequestResolver.batchN(options.embeddingBatchSize ?? 500),
|
|
73
83
|
)
|
|
84
|
+
const concurrency = options.concurrency ?? 2000
|
|
74
85
|
const indexHandle = yield* FiberHandle.make()
|
|
75
86
|
const console = yield* Console.Console
|
|
76
87
|
|
|
88
|
+
const resolveIndexedPath = (path: string): Option.Option<string> => {
|
|
89
|
+
const absolutePath = pathService.resolve(root, path)
|
|
90
|
+
const relativePath = normalizePath(
|
|
91
|
+
pathService.relative(root, absolutePath),
|
|
92
|
+
)
|
|
93
|
+
if (
|
|
94
|
+
relativePath.length === 0 ||
|
|
95
|
+
relativePath === ".." ||
|
|
96
|
+
relativePath.startsWith("../")
|
|
97
|
+
) {
|
|
98
|
+
return Option.none()
|
|
99
|
+
}
|
|
100
|
+
return Option.some(relativePath)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const processChunk = Effect.fnUntraced(
|
|
104
|
+
function* (options: {
|
|
105
|
+
readonly chunk: CodeChunker.CodeChunk
|
|
106
|
+
readonly syncId: ChunkRepo.SyncId
|
|
107
|
+
readonly checkExisting: boolean
|
|
108
|
+
}) {
|
|
109
|
+
if (options.checkExisting) {
|
|
110
|
+
const id = yield* repo.exists({
|
|
111
|
+
path: options.chunk.path,
|
|
112
|
+
startLine: options.chunk.startLine,
|
|
113
|
+
hash: options.chunk.contentHash,
|
|
114
|
+
})
|
|
115
|
+
if (Option.isSome(id)) {
|
|
116
|
+
yield* repo.setSyncId(id.value, options.syncId)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const result = yield* Effect.request(
|
|
122
|
+
new EmbeddingModel.EmbeddingRequest({
|
|
123
|
+
input: `File: ${options.chunk.path}
|
|
124
|
+
Lines: ${options.chunk.startLine}-${options.chunk.endLine}
|
|
125
|
+
|
|
126
|
+
${options.chunk.content}`,
|
|
127
|
+
}),
|
|
128
|
+
resolver,
|
|
129
|
+
)
|
|
130
|
+
const vector = new Float32Array(result.vector)
|
|
131
|
+
yield* repo.insert(
|
|
132
|
+
ChunkRepo.Chunk.insert.makeUnsafe({
|
|
133
|
+
path: options.chunk.path,
|
|
134
|
+
startLine: options.chunk.startLine,
|
|
135
|
+
endLine: options.chunk.endLine,
|
|
136
|
+
hash: options.chunk.contentHash,
|
|
137
|
+
content: options.chunk.content,
|
|
138
|
+
vector,
|
|
139
|
+
syncId: options.syncId,
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
},
|
|
143
|
+
Effect.ignore({
|
|
144
|
+
log: "Warn",
|
|
145
|
+
message: "Failed to process chunk for embedding",
|
|
146
|
+
}),
|
|
147
|
+
(effect, options) =>
|
|
148
|
+
Effect.annotateLogs(effect, {
|
|
149
|
+
chunk: `${options.chunk.path}/${options.chunk.startLine}`,
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
|
|
77
153
|
const index = Effect.gen(function* () {
|
|
78
154
|
const syncId = ChunkRepo.SyncId.makeUnsafe(crypto.randomUUID())
|
|
79
155
|
yield* Effect.logInfo("Starting SemanticSearch index")
|
|
80
156
|
|
|
81
157
|
yield* pipe(
|
|
82
158
|
chunker.chunkCodebase({
|
|
83
|
-
root
|
|
84
|
-
|
|
85
|
-
chunkOverlap: 5,
|
|
159
|
+
root,
|
|
160
|
+
...chunkConfig,
|
|
86
161
|
}),
|
|
87
162
|
Stream.tap(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
hash: chunk.contentHash,
|
|
94
|
-
})
|
|
95
|
-
if (Option.isSome(id)) {
|
|
96
|
-
yield* repo.setSyncId(id.value, syncId)
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
const result = yield* Effect.request(
|
|
100
|
-
new EmbeddingModel.EmbeddingRequest({
|
|
101
|
-
input: `File: ${chunk.path}
|
|
102
|
-
Lines: ${chunk.startLine}-${chunk.endLine}
|
|
103
|
-
|
|
104
|
-
${chunk.content}`,
|
|
105
|
-
}),
|
|
106
|
-
resolver,
|
|
107
|
-
)
|
|
108
|
-
const vector = new Float32Array(result.vector)
|
|
109
|
-
yield* repo.insert(
|
|
110
|
-
ChunkRepo.Chunk.insert.makeUnsafe({
|
|
111
|
-
path: chunk.path,
|
|
112
|
-
startLine: chunk.startLine,
|
|
113
|
-
endLine: chunk.endLine,
|
|
114
|
-
hash: chunk.contentHash,
|
|
115
|
-
content: chunk.content,
|
|
116
|
-
vector,
|
|
117
|
-
syncId,
|
|
118
|
-
}),
|
|
119
|
-
)
|
|
120
|
-
},
|
|
121
|
-
Effect.ignore({
|
|
122
|
-
log: "Warn",
|
|
123
|
-
message: "Failed to process chunk for embedding",
|
|
163
|
+
(chunk) =>
|
|
164
|
+
processChunk({
|
|
165
|
+
chunk,
|
|
166
|
+
syncId,
|
|
167
|
+
checkExisting: true,
|
|
124
168
|
}),
|
|
125
|
-
|
|
126
|
-
Effect.annotateLogs(effect, {
|
|
127
|
-
chunk: `${chunk.path}/${chunk.startLine}`,
|
|
128
|
-
}),
|
|
129
|
-
),
|
|
130
|
-
{ concurrency: options.concurrency ?? 2000 },
|
|
169
|
+
{ concurrency },
|
|
131
170
|
),
|
|
132
171
|
Stream.runDrain,
|
|
133
172
|
)
|
|
134
173
|
|
|
174
|
+
yield* repo.deleteForSyncId(syncId)
|
|
175
|
+
|
|
135
176
|
yield* Effect.logInfo("Finished SemanticSearch index")
|
|
136
177
|
}).pipe(
|
|
137
178
|
Effect.withSpan("SemanticSearch.index"),
|
|
@@ -161,7 +202,48 @@ ${chunk.content}`,
|
|
|
161
202
|
})
|
|
162
203
|
return results.map((r) => r.format()).join("\n\n")
|
|
163
204
|
}, Effect.orDie),
|
|
164
|
-
|
|
205
|
+
updateFile: Effect.fn("SemanticSearch.updateFile")(function* (path) {
|
|
206
|
+
yield* Fiber.join(initialIndex)
|
|
207
|
+
const indexedPath = resolveIndexedPath(path)
|
|
208
|
+
if (Option.isNone(indexedPath)) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
yield* repo.deleteByPath(indexedPath.value)
|
|
213
|
+
|
|
214
|
+
const chunks = yield* chunker.chunkFile({
|
|
215
|
+
root,
|
|
216
|
+
path: indexedPath.value,
|
|
217
|
+
...chunkConfig,
|
|
218
|
+
})
|
|
219
|
+
if (chunks.length === 0) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const syncId = ChunkRepo.SyncId.makeUnsafe(crypto.randomUUID())
|
|
224
|
+
|
|
225
|
+
yield* pipe(
|
|
226
|
+
Stream.fromArray(chunks),
|
|
227
|
+
Stream.tap(
|
|
228
|
+
(chunk) =>
|
|
229
|
+
processChunk({
|
|
230
|
+
chunk,
|
|
231
|
+
syncId,
|
|
232
|
+
checkExisting: false,
|
|
233
|
+
}),
|
|
234
|
+
{ concurrency },
|
|
235
|
+
),
|
|
236
|
+
Stream.runDrain,
|
|
237
|
+
)
|
|
238
|
+
}, Effect.orDie),
|
|
239
|
+
removeFile: Effect.fn("SemanticSearch.removeFile")(function* (path) {
|
|
240
|
+
yield* Fiber.join(initialIndex)
|
|
241
|
+
const indexedPath = resolveIndexedPath(path)
|
|
242
|
+
if (Option.isNone(indexedPath)) {
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
yield* repo.deleteByPath(indexedPath.value)
|
|
246
|
+
}, Effect.orDie),
|
|
165
247
|
})
|
|
166
248
|
}),
|
|
167
249
|
).pipe(
|
|
@@ -177,13 +259,26 @@ ${chunk.content}`,
|
|
|
177
259
|
* @since 1.0.0
|
|
178
260
|
* @category Utils
|
|
179
261
|
*/
|
|
180
|
-
export const
|
|
181
|
-
SemanticSearch
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
189
|
-
|
|
262
|
+
export const maybeUpdateFile = (path: string): Effect.Effect<void> =>
|
|
263
|
+
Effect.serviceOption(SemanticSearch).pipe(
|
|
264
|
+
Effect.flatMap(
|
|
265
|
+
Option.match({
|
|
266
|
+
onNone: () => Effect.void,
|
|
267
|
+
onSome: (service) => service.updateFile(path),
|
|
268
|
+
}),
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @since 1.0.0
|
|
274
|
+
* @category Utils
|
|
275
|
+
*/
|
|
276
|
+
export const maybeRemoveFile = (path: string): Effect.Effect<void> =>
|
|
277
|
+
Effect.serviceOption(SemanticSearch).pipe(
|
|
278
|
+
Effect.flatMap(
|
|
279
|
+
Option.match({
|
|
280
|
+
onNone: () => Effect.void,
|
|
281
|
+
onSome: (service) => service.removeFile(path),
|
|
282
|
+
}),
|
|
283
|
+
),
|
|
284
|
+
)
|