mcp: поиск индексирует коллекцию на холодном старте
This commit is contained in:
@@ -7,6 +7,7 @@ import memo.core.RefreshHook
|
||||
import memo.core.SearchMode
|
||||
import memo.core.Searcher
|
||||
import memo.core.findCollections
|
||||
import memo.core.isCollection
|
||||
import memo.core.resolveCollection
|
||||
import java.io.File
|
||||
|
||||
@@ -116,44 +117,54 @@ fun toolSearch(path: String, query: String, k: Int, mode: SearchMode): String {
|
||||
if (targets.isEmpty()) return "коллекции не найдены"
|
||||
val (modelPath, tokenizerPath) = modelPaths()
|
||||
val allHits = ArrayList<memo.core.Hit>()
|
||||
for (coll in targets) {
|
||||
val dbPath = File(coll, ".memo/index.db")
|
||||
if (!dbPath.exists()) continue
|
||||
runCatching {
|
||||
val errors = ArrayList<String>()
|
||||
val embedder = Embedder(modelPath, tokenizerPath)
|
||||
try {
|
||||
for (coll in targets) {
|
||||
val memoDir = File(coll, ".memo")
|
||||
val dbPath = File(memoDir, "index.db")
|
||||
memoDir.mkdirs()
|
||||
val existedBefore = dbPath.exists()
|
||||
val db = Db(dbPath.absolutePath)
|
||||
try {
|
||||
db.init()
|
||||
val embedder = Embedder(modelPath, tokenizerPath)
|
||||
try {
|
||||
val refresh = RefreshHook { r ->
|
||||
val innerDbPath = File(r, ".memo/index.db")
|
||||
if (innerDbPath.exists()) {
|
||||
val innerDb = Db(innerDbPath.absolutePath)
|
||||
try {
|
||||
innerDb.init()
|
||||
Indexer(innerDb, embedder).indexTree(r)
|
||||
} finally {
|
||||
innerDb.close()
|
||||
}
|
||||
if (!existedBefore) {
|
||||
Indexer(db, embedder).indexTree(coll)
|
||||
}
|
||||
val refresh = RefreshHook { r ->
|
||||
val innerDbPath = File(r, ".memo/index.db")
|
||||
if (innerDbPath.exists()) {
|
||||
val innerDb = Db(innerDbPath.absolutePath)
|
||||
try {
|
||||
innerDb.init()
|
||||
Indexer(innerDb, embedder).indexTree(r)
|
||||
} finally {
|
||||
innerDb.close()
|
||||
}
|
||||
}
|
||||
val hits = Searcher(db, embedder, refresh).search(coll, query, k, mode)
|
||||
allHits.addAll(hits)
|
||||
} finally {
|
||||
embedder.close()
|
||||
}
|
||||
val hits = Searcher(db, embedder, refresh).search(coll, query, k, mode)
|
||||
allHits.addAll(hits)
|
||||
} catch (t: Throwable) {
|
||||
errors.add("ошибка в ${coll.name}: ${t.message ?: t.javaClass.simpleName}")
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
embedder.close()
|
||||
}
|
||||
val merged = allHits.sortedByDescending { it.score }.take(k)
|
||||
if (merged.isEmpty()) return "ничего не найдено"
|
||||
val sb = StringBuilder()
|
||||
for (h in merged) {
|
||||
sb.append("${h.path}:${h.line} ${h.heading}\n${h.text}\n")
|
||||
}
|
||||
return sb.toString().trimEnd('\n')
|
||||
for (e in errors) {
|
||||
if (sb.isNotEmpty()) sb.append('\n')
|
||||
sb.append(e)
|
||||
}
|
||||
val out = sb.toString().trimEnd('\n')
|
||||
return if (out.isEmpty()) "ничего не найдено" else out
|
||||
}
|
||||
|
||||
fun toolStatus(path: String): String {
|
||||
@@ -191,33 +202,33 @@ fun toolReindex(path: String): String {
|
||||
if (collections.isEmpty()) return "коллекции не найдены"
|
||||
val (modelPath, tokenizerPath) = modelPaths()
|
||||
var totalUpdated = 0
|
||||
for (coll in collections) {
|
||||
val memoDir = File(coll, ".memo")
|
||||
if (memoDir.exists()) {
|
||||
memoDir.deleteRecursively()
|
||||
}
|
||||
memoDir.mkdirs()
|
||||
val dbPath = File(memoDir, "index.db")
|
||||
val db = Db(dbPath.absolutePath)
|
||||
try {
|
||||
db.init()
|
||||
val embedder = Embedder(modelPath, tokenizerPath)
|
||||
val embedder = Embedder(modelPath, tokenizerPath)
|
||||
try {
|
||||
for (coll in collections) {
|
||||
val memoDir = File(coll, ".memo")
|
||||
if (memoDir.exists()) {
|
||||
memoDir.deleteRecursively()
|
||||
}
|
||||
memoDir.mkdirs()
|
||||
val dbPath = File(memoDir, "index.db")
|
||||
val db = Db(dbPath.absolutePath)
|
||||
try {
|
||||
db.init()
|
||||
totalUpdated += Indexer(db, embedder).indexTree(coll)
|
||||
} finally {
|
||||
embedder.close()
|
||||
db.close()
|
||||
}
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
} finally {
|
||||
embedder.close()
|
||||
}
|
||||
return "переиндексировано файлов: $totalUpdated"
|
||||
}
|
||||
|
||||
private fun selectTargets(root: File): List<File> {
|
||||
if (!root.exists()) return emptyList()
|
||||
if (File(root, ".memo/index.db").exists()) return listOf(root)
|
||||
return findCollections(root).filter { File(it, ".memo/index.db").exists() }
|
||||
if (isCollection(root)) return listOf(root)
|
||||
return findCollections(root)
|
||||
}
|
||||
|
||||
private fun modelPaths(): Pair<String, String> {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package memo.mcp
|
||||
|
||||
import memo.core.SearchMode
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
class McpColdStartTest {
|
||||
|
||||
private fun modelPaths(): Pair<String, String> {
|
||||
val modelDir = System.getenv("MEMO_MODEL_DIR") ?: "/root/WORK/memo/models/siglip2"
|
||||
return "$modelDir/text_model_int8.onnx" to "$modelDir/tokenizer.model"
|
||||
}
|
||||
|
||||
private fun requireModel() {
|
||||
val (modelPath, tokenizerPath) = modelPaths()
|
||||
if (!File(modelPath).exists() || !File(tokenizerPath).exists()) {
|
||||
fail("модель не найдена: ${File(modelPath).parent}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchIndexesCollectionOnColdStart() {
|
||||
requireModel()
|
||||
|
||||
val base = File.createTempFile("memo-coldstart-", "")
|
||||
assertTrue(base.delete(), "temp cleanup")
|
||||
assertTrue(base.mkdir(), "temp create")
|
||||
base.deleteOnExit()
|
||||
|
||||
File(base, "proxy.md").writeText(
|
||||
"# Прокси\nвнутренние домены ходят через шлюз 76.1\n"
|
||||
)
|
||||
File(base, "other.md").writeText(
|
||||
"# Прочее\nсовсем другая заметка про книгу\n"
|
||||
)
|
||||
File(base, "proxy.md").deleteOnExit()
|
||||
File(base, "other.md").deleteOnExit()
|
||||
|
||||
val memoDir = File(base, ".memo")
|
||||
assertFalse(memoDir.exists(), "до поиска .memo не должен существовать")
|
||||
|
||||
val resp = toolSearch(base.absolutePath, "внутренние домены шлюз", 5, SearchMode.HYBRID)
|
||||
assertFalse(
|
||||
resp.contains("коллекции не найдены"),
|
||||
"холодный старт обязан сам индексировать, ответ: $resp"
|
||||
)
|
||||
assertTrue(
|
||||
resp.contains("76.1"),
|
||||
"ожидалось упоминание 76.1 в ответе, получено: $resp"
|
||||
)
|
||||
|
||||
val dbFile = File(base, ".memo/index.db")
|
||||
assertTrue(dbFile.exists(), "после поиска .memo/index.db обязан существовать")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchOnEmptyDirReportsNoCollections() {
|
||||
val base = File.createTempFile("memo-empty-", "")
|
||||
assertTrue(base.delete(), "temp cleanup")
|
||||
assertTrue(base.mkdir(), "temp create")
|
||||
base.deleteOnExit()
|
||||
|
||||
val resp = toolSearch(base.absolutePath, "что угодно", 5, SearchMode.HYBRID)
|
||||
assertEquals(
|
||||
"коллекции не найдены",
|
||||
resp,
|
||||
"пустой каталог без .md обязан вернуть 'коллекции не найдены'"
|
||||
)
|
||||
assertFalse(
|
||||
File(base, ".memo").exists(),
|
||||
"в пустом каталоге .memo не должен создаваться"
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user