Skip to content

Commit 742d1d1

Browse files
merge and bump version
2 parents 9c86462 + b4898f8 commit 742d1d1

7 files changed

Lines changed: 154 additions & 66 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Intelligence pipelines built upon vector-based retrieval can be _rigid and britt
4343

4444
## ✨ Key Features
4545

46-
### 1. Embedding-Free: Data in its Purest Form
46+
### 1. EmbeddingDB-Free: Data in its Purest Form
4747

4848
**Sirchmunk** works directly with **raw data** -- bypassing the heavy overhead of squeezing your rich files into fixed-dimensional vectors.
4949

@@ -150,6 +150,12 @@ It serves as a unified intelligent hub for AI agents, delivering deep insights a
150150

151151
## 🎉 News
152152

153+
* 🚀 **Mar 5, 2026**: Sirchmunk v0.0.5
154+
- **Breaking Change**: Unified Search API: Streamlined search() interface with a new SearchContext object and simplified parameter control (return_context).
155+
- **Robust RAG Chat**: Significantly improved conversational reliability through new retry mechanisms and granular exception handling.
156+
- **Stable MCP Integration**: Fixed mcp run initialization issues, ensuring seamless server deployment for Model Context Protocol users.
157+
- **PyPI Web UI Fix**: Corrected Next.js source bundling to support flawless Web UI startup for standard pip install users.
158+
153159
* 🚀 **Feb 27, 2026**: Sirchmunk v0.0.4
154160
- **Docker Support**: First-class Docker deployment with pre-built images for seamless containerized setup.
155161
- **FAST Search Mode**: New default greedy search mode using 2-level keyword cascade and context-window sampling — significantly faster retrieval with only 2 LLM calls (2-5s vs 10-30s).

README_zh.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
## ✨ 核心特性
4545

46-
### 1. 无需向量嵌入和预索引:直接面向原始数据形态
46+
### 1. 无需向量数据库和预索引:直接面向原始数据形态
4747

4848
**Sirchmunk** 直接处理 **原始数据** —— 无需将大量而繁杂的文件压缩为固定维度向量,或是构建为图数据库。
4949

@@ -148,6 +148,12 @@
148148

149149
## 🎉 News
150150

151+
* 🚀 **2026.3.5**: **Sirchmunk v0.0.5 发布**
152+
- **破坏性变更**:统一搜索 API:重构 search() 接口的返回类型,引入 SearchContext 对象并简化返回参数控制,API 调用更简洁。
153+
- **高可用 RAG 对话**:引入重试机制与细粒度异常处理,大幅提升了 RAG 聊天在复杂网络环境下的稳定性。
154+
- **稳定 MCP 集成**:修复 mcp run 初始化问题,确保 MCP 协议服务器在各环境下均能顺畅启动。
155+
- **PyPI 安装修复**:解决了标准 pip 安装后的 Web 源码定位问题,确保 Web UI 即装即用。
156+
151157
* 🚀 **2026.2.27**: **Sirchmunk v0.0.4 发布**
152158
- **Docker 部署支持**:提供预构建 Docker 镜像,支持容器化一键部署。
153159
- **FAST 检索模式**:新增默认贪心搜索模式,采用两级关键词级联与上下文窗口采样策略,仅需 2 次 LLM 调用(2-5s vs 10-30s),大幅提升检索速度。

src/sirchmunk/api/settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class UISettings(BaseModel):
8888

8989
class EnvironmentVariables(BaseModel):
9090
SIRCHMUNK_WORK_PATH: Optional[str] = None
91+
SIRCHMUNK_SEARCH_PATHS: Optional[str] = None
9192
LLM_BASE_URL: Optional[str] = None
9293
LLM_API_KEY: Optional[str] = None
9394
LLM_MODEL_NAME: Optional[str] = None
@@ -115,6 +116,13 @@ def get_current_env_variables() -> Dict[str, Any]:
115116
"description": "Working directory for Sirchmunk data",
116117
"category": "system"
117118
},
119+
"SIRCHMUNK_SEARCH_PATHS": {
120+
"value": os.getenv("SIRCHMUNK_SEARCH_PATHS", ""),
121+
"default": "",
122+
"description": "Default search paths (comma-separated). "
123+
"Overridden by explicit paths passed to search().",
124+
"category": "search"
125+
},
118126
"LLM_BASE_URL": {
119127
"value": os.getenv("LLM_BASE_URL", _DEFAULT_LLM_BASE_URL),
120128
"default": _DEFAULT_LLM_BASE_URL,

src/sirchmunk/cli/cli.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import os
2121
import sys
2222
from pathlib import Path
23+
from typing import Optional
2324

2425
from sirchmunk.version import __version__
2526

@@ -119,6 +120,10 @@ def _generate_env_file(env_file: Path):
119120
SIRCHMUNK_VERBOSE=false
120121
121122
# ===== Search Settings =====
123+
# Default search paths (comma-separated), e.g. /data/docs,/data/pdfs
124+
# When set, acts as the default for search(..., paths=...) if none are provided.
125+
SIRCHMUNK_SEARCH_PATHS=
126+
122127
# Maximum directory depth to search
123128
DEFAULT_MAX_DEPTH=5
124129
@@ -470,7 +475,7 @@ def cmd_search(args: argparse.Namespace) -> int:
470475
_load_env_file(env_file)
471476

472477
query = args.query
473-
paths = args.paths or [os.getcwd()]
478+
paths = args.paths or None
474479

475480
if args.api:
476481
# Client mode: call API server
@@ -502,7 +507,7 @@ def cmd_search(args: argparse.Namespace) -> int:
502507

503508
async def _search_local(
504509
query: str,
505-
paths: list,
510+
paths: Optional[list] = None,
506511
mode: str = "FAST",
507512
output_format: str = "text",
508513
verbose: bool = False,
@@ -511,7 +516,7 @@ async def _search_local(
511516
512517
Args:
513518
query: Search query
514-
paths: Paths to search
519+
paths: Paths to search (None lets AgenticSearch resolve via env/cwd fallback)
515520
mode: Search mode (FAST, DEEP, FILENAME_ONLY)
516521
output_format: Output format (text, json)
517522
verbose: Enable verbose output
@@ -551,7 +556,8 @@ async def _search_local(
551556
if not verbose:
552557
print(f" Searching: {query}")
553558
print(f" Mode: {mode}")
554-
print(f" Paths: {', '.join(paths)}")
559+
if paths:
560+
print(f" Paths: {', '.join(paths)}")
555561
print()
556562

557563
# Execute search
@@ -584,7 +590,7 @@ async def _search_local(
584590

585591
def _search_via_api(
586592
query: str,
587-
paths: list,
593+
paths: Optional[list] = None,
588594
api_url: str = "http://localhost:8584",
589595
mode: str = "FAST",
590596
output_format: str = "text",
@@ -593,7 +599,7 @@ def _search_via_api(
593599
594600
Args:
595601
query: Search query
596-
paths: Paths to search
602+
paths: Paths to search (None lets server resolve via env/cwd)
597603
api_url: API server URL
598604
mode: Search mode
599605
output_format: Output format

src/sirchmunk/search.py

Lines changed: 91 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,13 @@ def _resolve_paths(
173173
self,
174174
paths: Optional[Union[str, Path, List[str], List[Path]]],
175175
) -> List[str]:
176-
"""Resolve and normalise paths: arg > self.paths > cwd.
176+
"""Resolve and normalise paths with layered fallback.
177+
178+
Priority (highest → lowest):
179+
1. Explicit ``paths`` argument (``search(..., paths=xxx)``)
180+
2. Instance default ``self.paths`` (constructor ``paths=``)
181+
3. ``SIRCHMUNK_SEARCH_PATHS`` environment variable (comma-separated)
182+
4. Current working directory
177183
178184
Always returns ``List[str]`` so callers need no further coercion.
179185
"""
@@ -183,6 +189,14 @@ def _resolve_paths(
183189
return [str(p) for p in paths]
184190
if self.paths is not None:
185191
return list(self.paths)
192+
env_paths = os.getenv("SIRCHMUNK_SEARCH_PATHS", "")
193+
if env_paths:
194+
parsed = [p.strip() for p in env_paths.split(",") if p.strip()]
195+
if parsed:
196+
_loguru_logger.info(
197+
f"[paths] Using SIRCHMUNK_SEARCH_PATHS: {parsed}"
198+
)
199+
return parsed
186200
cwd = str(Path.cwd())
187201
_loguru_logger.info(
188202
f"[paths] No paths provided; using current working directory: {cwd}"
@@ -285,81 +299,56 @@ def _load_historical_knowledge(self):
285299
async def _try_reuse_cluster(self, query: str) -> Optional[KnowledgeCluster]:
286300
"""Try to reuse existing knowledge cluster based on semantic similarity.
287301
302+
The method waits (non-blocking) for the embedding model to become
303+
ready so that reuse works reliably even on the first search call
304+
within a process.
305+
288306
Returns:
289307
KnowledgeCluster if a suitable cached cluster is found, None otherwise.
290308
"""
291309
if not self.embedding_client:
292310
return None
293311

294-
# Skip cluster reuse while the embedding model is still loading in
295-
# its background thread; kick off loading so it's ready next time.
296-
if not self.embedding_client.is_ready():
297-
self.embedding_client.start_loading()
298-
return None
299-
300312
try:
313+
# Wait for the model (non-blocking via executor) instead of
314+
# returning None immediately — this ensures reuse works even
315+
# on the very first search call.
316+
if not self.embedding_client.is_ready():
317+
self.embedding_client.start_loading()
318+
try:
319+
await self.embedding_client._ensure_model_async(timeout=60)
320+
except Exception:
321+
await self._logger.debug(
322+
"Embedding model not ready yet, skipping cluster reuse"
323+
)
324+
return None
325+
301326
await self._logger.info("Searching for similar knowledge clusters...")
302-
303-
# Compute query embedding
327+
304328
query_embedding = (await self.embedding_client.embed([query]))[0]
305-
306-
# Search for similar clusters
329+
307330
similar_clusters = await self.knowledge_storage.search_similar_clusters(
308331
query_embedding=query_embedding,
309332
top_k=self.cluster_sim_top_k,
310333
similarity_threshold=self.cluster_sim_threshold,
311334
)
312-
335+
313336
if not similar_clusters:
314337
await self._logger.info("No similar clusters found, performing new search...")
315338
return None
316-
317-
# Found similar cluster - process reuse
339+
318340
best_match = similar_clusters[0]
319341
await self._logger.success(
320342
f"♻️ Found similar cluster: {best_match['name']} "
321343
f"(similarity: {best_match['similarity']:.3f})"
322344
)
323-
324-
# Retrieve full cluster object
345+
325346
existing_cluster = await self.knowledge_storage.get(best_match["id"])
326-
327347
if not existing_cluster:
328348
await self._logger.warning("Failed to retrieve cluster, falling back to new search")
329349
return None
330-
331-
# Add current query to queries list with FIFO strategy
332-
self._add_query_to_cluster(existing_cluster, query)
333-
334-
# Update hotness and timestamp for reused cluster
335-
existing_cluster.hotness = min(1.0, (existing_cluster.hotness or 0.5) + 0.1)
336-
existing_cluster.last_modified = datetime.now()
337-
338-
# Recompute embedding with new query (before update to avoid double save)
339-
if self.embedding_client and self.embedding_client.is_ready():
340-
try:
341-
from sirchmunk.utils.embedding_util import compute_text_hash
342350

343-
combined_text = self.knowledge_storage.combine_cluster_fields(
344-
existing_cluster.queries
345-
)
346-
text_hash = compute_text_hash(combined_text)
347-
embedding_vector = (await self.embedding_client.embed([combined_text]))[0]
348-
349-
await self.knowledge_storage.store_embedding(
350-
cluster_id=existing_cluster.id,
351-
embedding_vector=embedding_vector,
352-
embedding_model=self.embedding_client.model_id,
353-
embedding_text_hash=text_hash,
354-
)
355-
await self._logger.debug(f"Updated embedding for cluster {existing_cluster.id}")
356-
except Exception as emb_error:
357-
await self._logger.warning(f"Failed to update embedding: {emb_error}")
358-
359-
# Single update call - saves cluster data and embedding together
360-
await self.knowledge_storage.update(existing_cluster)
361-
362-
# Validate cluster has usable content
351+
# Validate cluster has usable content BEFORE mutating it
363352
content = existing_cluster.content
364353
if isinstance(content, list):
365354
content = "\n".join(content)
@@ -369,9 +358,41 @@ async def _try_reuse_cluster(self, query: str) -> Optional[KnowledgeCluster]:
369358
)
370359
return None
371360

361+
# Mutate only after validation passes
362+
self._add_query_to_cluster(existing_cluster, query)
363+
existing_cluster.hotness = min(1.0, (existing_cluster.hotness or 0.5) + 0.1)
364+
existing_cluster.last_modified = datetime.now()
365+
366+
# Recompute embedding with updated queries list
367+
try:
368+
from sirchmunk.utils.embedding_util import compute_text_hash
369+
370+
combined_text = self.knowledge_storage.combine_cluster_fields(
371+
existing_cluster.queries
372+
)
373+
text_hash = compute_text_hash(combined_text)
374+
embedding_vector = (await self.embedding_client.embed([combined_text]))[0]
375+
376+
await self.knowledge_storage.store_embedding(
377+
cluster_id=existing_cluster.id,
378+
embedding_vector=embedding_vector,
379+
embedding_model=self.embedding_client.model_id,
380+
embedding_text_hash=text_hash,
381+
)
382+
except Exception as emb_error:
383+
await self._logger.warning(f"Failed to update embedding: {emb_error}")
384+
385+
await self.knowledge_storage.update(existing_cluster)
386+
387+
# Flush to parquet so the updated cluster is visible to future searches
388+
try:
389+
self.knowledge_storage.force_sync()
390+
except Exception as sync_err:
391+
await self._logger.warning(f"Parquet force_sync failed: {sync_err}")
392+
372393
await self._logger.success("Reused existing knowledge cluster")
373394
return existing_cluster
374-
395+
375396
except Exception as e:
376397
await self._logger.warning(
377398
f"Failed to search similar clusters: {e}. Falling back to full search."
@@ -406,12 +427,17 @@ async def _save_cluster_with_embedding(self, cluster: KnowledgeCluster) -> None:
406427
Args:
407428
cluster: KnowledgeCluster to save
408429
"""
409-
# Save knowledge cluster to persistent storage
430+
# Save knowledge cluster to persistent storage.
431+
# insert() returns False (without raising) when the cluster already
432+
# exists, so we explicitly fall back to update() in that case.
410433
try:
411-
await self.knowledge_storage.insert(cluster)
412-
await self._logger.info(f"Saved knowledge cluster {cluster.id} to cache")
434+
inserted = await self.knowledge_storage.insert(cluster)
435+
if inserted:
436+
await self._logger.info(f"Saved knowledge cluster {cluster.id} to cache")
437+
else:
438+
await self.knowledge_storage.update(cluster)
439+
await self._logger.info(f"Updated knowledge cluster {cluster.id} in cache")
413440
except Exception as e:
414-
# If cluster exists, update it instead
415441
try:
416442
await self.knowledge_storage.update(cluster)
417443
await self._logger.info(f"Updated knowledge cluster {cluster.id} in cache")
@@ -978,6 +1004,8 @@ async def _search_deep(
9781004

9791005
# ==============================================================
9801006
# Phase 0: Cluster reuse (instant short-circuit)
1007+
# When reuse_knowledge=True and a similar cluster is found, we
1008+
# return here — Phase 5 (Persistence) is not executed for that path.
9811009
# ==============================================================
9821010
reused = await self._try_reuse_cluster(query)
9831011
if reused is not None:
@@ -1116,14 +1144,19 @@ async def _search_deep(
11161144
context.add_llm_tokens(total_tok, usage=usage)
11171145

11181146
# ==============================================================
1119-
# Phase 5: Persistence
1147+
# Phase 5: Persistence (only when no cluster was reused in Phase 0)
1148+
# When Phase 0 reuses a cluster we return early, so this block
1149+
# runs only for newly built clusters from Phase 1–4.
11201150
# ==============================================================
11211151
phase5_tasks = []
11221152
if cluster:
11231153
self._add_query_to_cluster(cluster, query)
11241154
phase5_tasks.append(self._save_cluster_with_embedding(cluster))
11251155
phase5_tasks.append(self._save_spec_context(paths, context, scan_result=scan_result))
1126-
await asyncio.gather(*phase5_tasks, return_exceptions=True)
1156+
results = await asyncio.gather(*phase5_tasks, return_exceptions=True)
1157+
for r in results:
1158+
if isinstance(r, Exception):
1159+
_loguru_logger.warning(f"[Phase 5] Persistence task failed: {r}")
11271160

11281161
await self._logger.success(f"[search] Complete: {context.summary()}")
11291162
return answer, cluster, context
@@ -1219,6 +1252,7 @@ async def _search_fast(
12191252

12201253
# ==============================================================
12211254
# Step 0: Cluster reuse — instant short-circuit (no LLM cost)
1255+
# When reuse succeeds we return here; no persistence step runs.
12221256
# ==============================================================
12231257
reused = await self._try_reuse_cluster(query)
12241258
if reused is not None:
@@ -1336,7 +1370,7 @@ async def _search_fast(
13361370
query, answer, file_path, evidence, keywords_used,
13371371
)
13381372

1339-
# Persist the FAST cluster so it can be reused by future queries
1373+
# Persist the new cluster (only reached when Step 0 did not reuse)
13401374
self._add_query_to_cluster(cluster, query)
13411375
try:
13421376
await self._save_cluster_with_embedding(cluster)

src/sirchmunk/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.5"
1+
__version__ = "0.0.5post1"

0 commit comments

Comments
 (0)