Skip to content

Commit cfc3e61

Browse files
committed
feat: enhance CLI output and improve knowledge store handling in MCP server
1 parent 9487bf8 commit cfc3e61

4 files changed

Lines changed: 75 additions & 18 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

src/knowcode/cli/cli.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,14 @@ def analyze(directory: str, output: str, ignore: tuple[str, ...], temporal: bool
6767
click.echo(f" Relationships: {stats['total_relationships']}")
6868
if stats.get('total_errors', 0) > 0:
6969
click.echo(f" Errors: {stats['total_errors']}")
70+
if stats.get("indexed_chunks") is not None:
71+
click.echo(f" Indexed chunks: {stats['indexed_chunks']}")
7072

7173
output_path = Path(output)
7274
save_path = output_path / KnowledgeStore.DEFAULT_FILENAME if output_path.is_dir() else output_path
7375
click.echo(f"\n Saved to: {save_path}")
76+
if stats.get("index_path"):
77+
click.echo(f" Index saved to: {stats['index_path']}")
7478

7579

7680
@cli.command()
@@ -557,15 +561,12 @@ def mcp_server(store: str, config: Optional[str]) -> None:
557561
}
558562
"""
559563
store_path = Path(store)
560-
if store_path.is_dir():
561-
store_path = store_path / KnowledgeStore.DEFAULT_FILENAME
562-
563-
if not store_path.exists():
564+
store_file = store_path / KnowledgeStore.DEFAULT_FILENAME if store_path.is_dir() else store_path
565+
if not store_file.exists():
564566
click.echo(
565-
"Error: Knowledge store not found. Run 'knowcode analyze' first.",
566-
err=True
567+
"⚠️ Knowledge store not found. It will be built on first query.",
568+
err=True,
567569
)
568-
sys.exit(1)
569570

570571
try:
571572
from knowcode.mcp.server import run_server

src/knowcode/mcp/server.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,14 @@ def __init__(self, store_path: str | Path, config_path: Optional[str] = None) ->
156156
self.config_path = config_path
157157
self._service: Optional[KnowCodeService] = None
158158

159-
def _ensure_service(self) -> KnowCodeService:
159+
def _ensure_service(self, allow_missing_store: bool = False) -> KnowCodeService:
160160
"""Create the shared service (loads store/index lazily)."""
161161
if self._service is None:
162162
store_file = self.store_path
163163
if store_file.is_dir():
164164
store_file = store_file / KnowledgeStore.DEFAULT_FILENAME
165165

166-
if not store_file.exists():
166+
if not allow_missing_store and not store_file.exists():
167167
raise FileNotFoundError(
168168
f"Knowledge store not found: {store_file}\n"
169169
"Run 'knowcode analyze' first to build the knowledge graph."
@@ -174,6 +174,14 @@ def _ensure_service(self) -> KnowCodeService:
174174
config_path=self.config_path,
175175
)
176176
return self._service
177+
178+
def _ensure_store_ready(self, service: KnowCodeService) -> None:
179+
"""Ensure the knowledge store exists by running analyze if needed."""
180+
store_root = service.store_path if service.store_path.is_dir() else service.store_path.parent
181+
store_file = store_root / KnowledgeStore.DEFAULT_FILENAME
182+
if store_file.exists():
183+
return
184+
service.analyze(directory=store_root, output=store_root)
177185

178186
def search_codebase(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
179187
"""Search for entities by name pattern.
@@ -185,7 +193,8 @@ def search_codebase(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
185193
Returns:
186194
List of matching entity summaries.
187195
"""
188-
service = self._ensure_service()
196+
service = self._ensure_service(allow_missing_store=True)
197+
self._ensure_store_ready(service)
189198
entities = service.store.search(query)[:limit]
190199

191200
return [
@@ -216,7 +225,8 @@ def get_entity_context(
216225
Returns:
217226
Context bundle with sufficiency score.
218227
"""
219-
service = self._ensure_service()
228+
service = self._ensure_service(allow_missing_store=True)
229+
self._ensure_store_ready(service)
220230
try:
221231
task = TaskType(task_type)
222232
except ValueError:
@@ -260,7 +270,8 @@ def trace_calls(
260270
Returns:
261271
List of entities with call_depth.
262272
"""
263-
service = self._ensure_service()
273+
service = self._ensure_service(allow_missing_store=True)
274+
self._ensure_store_ready(service)
264275
return service.store.trace_calls(
265276
entity_id,
266277
direction=direction,
@@ -277,7 +288,7 @@ def retrieve_context_for_query(
277288
expand_deps: bool = True,
278289
) -> dict[str, Any]:
279290
"""Retrieve a task-aware context bundle for a query."""
280-
service = self._ensure_service()
291+
service = self._ensure_service(allow_missing_store=True)
281292
task_override: Optional[TaskType] = None
282293
if task_type != "auto":
283294
try:

src/knowcode/service.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,34 @@ def retrieve_context_for_query(
125125
from knowcode.llm.query_classifier import classify_query
126126

127127
errors: list[str] = []
128+
store_root = self.store_path if self.store_path.is_dir() else self.store_path.parent
129+
store_file = store_root / KnowledgeStore.DEFAULT_FILENAME
130+
index_path = store_root / "knowcode_index"
131+
132+
if not store_file.exists():
133+
try:
134+
self.analyze(directory=store_root, output=store_root)
135+
except Exception as e:
136+
return {
137+
"query": query,
138+
"task_type": task_type.value if task_type else "general",
139+
"task_confidence": 0.0,
140+
"retrieval_mode": "none",
141+
"context_text": "",
142+
"total_tokens": 0,
143+
"max_tokens": max_tokens,
144+
"truncated": False,
145+
"sufficiency_score": 0.0,
146+
"selected_entities": [],
147+
"evidence": [],
148+
"errors": [f"Auto-analyze failed: {e}"],
149+
}
150+
151+
if not index_path.exists():
152+
try:
153+
self._build_index(store_root, index_path)
154+
except Exception as e:
155+
errors.append(f"Auto-index failed; falling back to lexical: {e}")
128156

129157
detected_task_type, confidence = classify_query(query)
130158
resolved_task_type = task_type or detected_task_type
@@ -149,9 +177,6 @@ def retrieve_context_for_query(
149177
if per_entity_max_tokens is None:
150178
per_entity_max_tokens = max(200, min(2000, max_tokens // limit_entities))
151179

152-
store_root = self.store_path if self.store_path.is_dir() else self.store_path.parent
153-
index_path = store_root / "knowcode_index"
154-
155180
selected_entity_ids: list[str] = []
156181
evidence: list[dict[str, Any]] = []
157182
retrieval_mode = "lexical"
@@ -272,6 +297,18 @@ def add_entity_ids(items: list[dict[str, Any]]) -> None:
272297
"errors": errors,
273298
}
274299

300+
def _build_index(self, directory: str | Path, index_path: str | Path) -> int:
301+
"""Build a semantic index for a directory and persist it."""
302+
from knowcode.llm.embedding import create_embedding_provider
303+
from knowcode.indexing.indexer import Indexer
304+
305+
provider = create_embedding_provider(app_config=self.app_config)
306+
indexer = Indexer(provider)
307+
count = indexer.index_directory(directory)
308+
indexer.save(index_path)
309+
self._indexer = indexer
310+
return count
311+
275312
def _extract_query_keywords(self, query: str) -> list[str]:
276313
"""Extract identifier-like keywords from a natural-language query."""
277314
stopwords = {
@@ -381,8 +418,15 @@ def analyze(
381418
output_path = Path(output)
382419
store.save(output_path)
383420
self._store = store
384-
385-
return builder.stats()
421+
422+
store_root = output_path if output_path.is_dir() else output_path.parent
423+
index_path = store_root / "knowcode_index"
424+
index_count = self._build_index(Path(directory), index_path)
425+
426+
stats = builder.stats()
427+
stats["indexed_chunks"] = index_count
428+
stats["index_path"] = str(index_path)
429+
return stats
386430

387431
def search(self, pattern: str) -> list[dict[str, Any]]:
388432
"""Search entities by pattern.

0 commit comments

Comments
 (0)