code-graph-builder 0.2.0__py3-none-any.whl
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.
- code_graph_builder/__init__.py +82 -0
- code_graph_builder/builder.py +366 -0
- code_graph_builder/cgb_cli.py +32 -0
- code_graph_builder/cli.py +564 -0
- code_graph_builder/commands_cli.py +1288 -0
- code_graph_builder/config.py +340 -0
- code_graph_builder/constants.py +708 -0
- code_graph_builder/embeddings/__init__.py +40 -0
- code_graph_builder/embeddings/qwen3_embedder.py +573 -0
- code_graph_builder/embeddings/vector_store.py +584 -0
- code_graph_builder/examples/__init__.py +0 -0
- code_graph_builder/examples/example_configuration.py +276 -0
- code_graph_builder/examples/example_kuzu_usage.py +109 -0
- code_graph_builder/examples/example_semantic_search_full.py +347 -0
- code_graph_builder/examples/generate_wiki.py +915 -0
- code_graph_builder/examples/graph_export_example.py +100 -0
- code_graph_builder/examples/rag_example.py +206 -0
- code_graph_builder/examples/test_cli_demo.py +129 -0
- code_graph_builder/examples/test_embedding_api.py +153 -0
- code_graph_builder/examples/test_kuzu_local.py +190 -0
- code_graph_builder/examples/test_rag_redis.py +390 -0
- code_graph_builder/graph_updater.py +605 -0
- code_graph_builder/guidance/__init__.py +1 -0
- code_graph_builder/guidance/agent.py +123 -0
- code_graph_builder/guidance/prompts.py +74 -0
- code_graph_builder/guidance/toolset.py +264 -0
- code_graph_builder/language_spec.py +536 -0
- code_graph_builder/mcp/__init__.py +21 -0
- code_graph_builder/mcp/api_doc_generator.py +764 -0
- code_graph_builder/mcp/file_editor.py +207 -0
- code_graph_builder/mcp/pipeline.py +777 -0
- code_graph_builder/mcp/server.py +161 -0
- code_graph_builder/mcp/tools.py +1800 -0
- code_graph_builder/models.py +115 -0
- code_graph_builder/parser_loader.py +344 -0
- code_graph_builder/parsers/__init__.py +7 -0
- code_graph_builder/parsers/call_processor.py +306 -0
- code_graph_builder/parsers/call_resolver.py +139 -0
- code_graph_builder/parsers/definition_processor.py +796 -0
- code_graph_builder/parsers/factory.py +119 -0
- code_graph_builder/parsers/import_processor.py +293 -0
- code_graph_builder/parsers/structure_processor.py +145 -0
- code_graph_builder/parsers/type_inference.py +143 -0
- code_graph_builder/parsers/utils.py +134 -0
- code_graph_builder/rag/__init__.py +68 -0
- code_graph_builder/rag/camel_agent.py +429 -0
- code_graph_builder/rag/client.py +298 -0
- code_graph_builder/rag/config.py +239 -0
- code_graph_builder/rag/cypher_generator.py +67 -0
- code_graph_builder/rag/llm_backend.py +210 -0
- code_graph_builder/rag/markdown_generator.py +352 -0
- code_graph_builder/rag/prompt_templates.py +440 -0
- code_graph_builder/rag/rag_engine.py +640 -0
- code_graph_builder/rag/review_report.md +172 -0
- code_graph_builder/rag/tests/__init__.py +3 -0
- code_graph_builder/rag/tests/test_camel_agent.py +313 -0
- code_graph_builder/rag/tests/test_client.py +221 -0
- code_graph_builder/rag/tests/test_config.py +177 -0
- code_graph_builder/rag/tests/test_markdown_generator.py +240 -0
- code_graph_builder/rag/tests/test_prompt_templates.py +160 -0
- code_graph_builder/services/__init__.py +39 -0
- code_graph_builder/services/graph_service.py +465 -0
- code_graph_builder/services/kuzu_service.py +665 -0
- code_graph_builder/services/memory_service.py +171 -0
- code_graph_builder/settings.py +75 -0
- code_graph_builder/tests/ACCEPTANCE_CRITERIA_PHASE2.md +401 -0
- code_graph_builder/tests/__init__.py +1 -0
- code_graph_builder/tests/run_acceptance_check.py +378 -0
- code_graph_builder/tests/test_api_find.py +231 -0
- code_graph_builder/tests/test_api_find_integration.py +226 -0
- code_graph_builder/tests/test_basic.py +78 -0
- code_graph_builder/tests/test_c_api_extraction.py +388 -0
- code_graph_builder/tests/test_call_resolution_scenarios.py +504 -0
- code_graph_builder/tests/test_embedder.py +411 -0
- code_graph_builder/tests/test_integration_semantic.py +434 -0
- code_graph_builder/tests/test_mcp_protocol.py +298 -0
- code_graph_builder/tests/test_mcp_user_flow.py +190 -0
- code_graph_builder/tests/test_rag.py +404 -0
- code_graph_builder/tests/test_settings.py +135 -0
- code_graph_builder/tests/test_step1_graph_build.py +264 -0
- code_graph_builder/tests/test_step2_api_docs.py +323 -0
- code_graph_builder/tests/test_step3_embedding.py +278 -0
- code_graph_builder/tests/test_vector_store.py +552 -0
- code_graph_builder/tools/__init__.py +40 -0
- code_graph_builder/tools/graph_query.py +495 -0
- code_graph_builder/tools/semantic_search.py +387 -0
- code_graph_builder/types.py +333 -0
- code_graph_builder/utils/__init__.py +0 -0
- code_graph_builder/utils/path_utils.py +30 -0
- code_graph_builder-0.2.0.dist-info/METADATA +321 -0
- code_graph_builder-0.2.0.dist-info/RECORD +93 -0
- code_graph_builder-0.2.0.dist-info/WHEEL +4 -0
- code_graph_builder-0.2.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,1800 @@
|
|
|
1
|
+
"""MCP tool registry and handler implementations for Code Graph Builder.
|
|
2
|
+
|
|
3
|
+
Architecture: workspace-based, dynamic service loading.
|
|
4
|
+
|
|
5
|
+
Workspace layout:
|
|
6
|
+
{CGB_WORKSPACE}/ default: ~/.code-graph-builder/
|
|
7
|
+
active.txt name of the currently active artifact dir
|
|
8
|
+
{repo_name}_{hash8}/
|
|
9
|
+
meta.json {repo_path, indexed_at, wiki_page_count}
|
|
10
|
+
graph.db KùzuDB database
|
|
11
|
+
vectors.pkl embedding cache
|
|
12
|
+
{repo_name}_structure.pkl wiki structure cache
|
|
13
|
+
wiki/
|
|
14
|
+
index.md
|
|
15
|
+
wiki/
|
|
16
|
+
page-1.md
|
|
17
|
+
...
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import json
|
|
24
|
+
import pickle
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from loguru import logger
|
|
30
|
+
|
|
31
|
+
from ..embeddings.qwen3_embedder import Qwen3Embedder
|
|
32
|
+
from ..embeddings.vector_store import MemoryVectorStore, VectorRecord
|
|
33
|
+
from ..rag.cypher_generator import CypherGenerator
|
|
34
|
+
from ..rag.llm_backend import create_llm_backend
|
|
35
|
+
from ..services.kuzu_service import KuzuIngestor
|
|
36
|
+
from ..tools.semantic_search import SemanticSearchService
|
|
37
|
+
from .file_editor import FileEditor
|
|
38
|
+
from .pipeline import (
|
|
39
|
+
ProgressCb,
|
|
40
|
+
artifact_dir_for,
|
|
41
|
+
build_graph,
|
|
42
|
+
build_vector_index,
|
|
43
|
+
generate_api_docs_step,
|
|
44
|
+
generate_descriptions_step,
|
|
45
|
+
run_wiki_generation,
|
|
46
|
+
save_meta,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ToolDefinition:
|
|
52
|
+
name: str
|
|
53
|
+
description: str
|
|
54
|
+
input_schema: dict[str, Any]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ToolError(Exception):
|
|
58
|
+
"""Error raised by tool handlers.
|
|
59
|
+
|
|
60
|
+
The MCP framework catches exceptions and returns ``CallToolResult`` with
|
|
61
|
+
``isError=True``, so the agent can detect errors via the protocol-level
|
|
62
|
+
flag instead of having to parse JSON response bodies.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, error_data: dict[str, Any] | str) -> None:
|
|
66
|
+
if isinstance(error_data, str):
|
|
67
|
+
error_data = {"error": error_data}
|
|
68
|
+
self.error_data = error_data
|
|
69
|
+
super().__init__(json.dumps(error_data, ensure_ascii=False, default=str))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _load_vector_store(vectors_path: Path) -> MemoryVectorStore:
|
|
73
|
+
"""Load MemoryVectorStore from a pickle cache file."""
|
|
74
|
+
if not vectors_path.exists():
|
|
75
|
+
raise FileNotFoundError(f"Vectors file not found: {vectors_path}")
|
|
76
|
+
|
|
77
|
+
with open(vectors_path, "rb") as fh:
|
|
78
|
+
data = pickle.load(fh)
|
|
79
|
+
|
|
80
|
+
if isinstance(data, dict) and "vector_store" in data:
|
|
81
|
+
store = data["vector_store"]
|
|
82
|
+
if isinstance(store, MemoryVectorStore):
|
|
83
|
+
return store
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
f"'vector_store' key found but value is not MemoryVectorStore: {type(store)}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if not isinstance(data, list) or len(data) == 0:
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
f"Unexpected vectors file content: expected non-empty list, got {type(data)}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
first = data[0]
|
|
94
|
+
if isinstance(first, VectorRecord):
|
|
95
|
+
dimension = len(first.embedding)
|
|
96
|
+
store = MemoryVectorStore(dimension=dimension)
|
|
97
|
+
store.store_embeddings_batch(data)
|
|
98
|
+
return store
|
|
99
|
+
|
|
100
|
+
if isinstance(first, dict) and "embedding" in first:
|
|
101
|
+
dimension = len(first["embedding"])
|
|
102
|
+
store = MemoryVectorStore(dimension=dimension)
|
|
103
|
+
for idx, item in enumerate(data):
|
|
104
|
+
store.store_embedding(
|
|
105
|
+
node_id=item.get("node_id", idx),
|
|
106
|
+
qualified_name=item.get("qualified_name", str(idx)),
|
|
107
|
+
embedding=item["embedding"],
|
|
108
|
+
metadata={
|
|
109
|
+
k: v
|
|
110
|
+
for k, v in item.items()
|
|
111
|
+
if k not in ("node_id", "qualified_name", "embedding")
|
|
112
|
+
and isinstance(v, (str, int, float, type(None)))
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
return store
|
|
116
|
+
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
f"Unrecognised vectors file format. First element type: {type(first)}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class MCPToolsRegistry:
|
|
123
|
+
"""Registry that manages workspace-based repo services and tool handlers."""
|
|
124
|
+
|
|
125
|
+
def __init__(self, workspace: Path) -> None:
|
|
126
|
+
self._workspace = workspace
|
|
127
|
+
self._workspace.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
self._ingestor: KuzuIngestor | None = None
|
|
130
|
+
self._cypher_gen: CypherGenerator | None = None
|
|
131
|
+
self._semantic_service: SemanticSearchService | None = None
|
|
132
|
+
self._file_editor: FileEditor | None = None
|
|
133
|
+
self._active_repo_path: Path | None = None
|
|
134
|
+
self._active_artifact_dir: Path | None = None
|
|
135
|
+
|
|
136
|
+
self._try_auto_load()
|
|
137
|
+
|
|
138
|
+
def _try_auto_load(self) -> None:
|
|
139
|
+
"""Try to load the last active repo from workspace."""
|
|
140
|
+
active_file = self._workspace / "active.txt"
|
|
141
|
+
if not active_file.exists():
|
|
142
|
+
return
|
|
143
|
+
artifact_dir_name = active_file.read_text(encoding="utf-8").strip()
|
|
144
|
+
artifact_dir = self._workspace / artifact_dir_name
|
|
145
|
+
if artifact_dir.exists():
|
|
146
|
+
try:
|
|
147
|
+
self._load_services(artifact_dir)
|
|
148
|
+
logger.info(f"Auto-loaded repo from: {artifact_dir}")
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
logger.warning(f"Graph/LLM services unavailable: {exc}")
|
|
151
|
+
|
|
152
|
+
def _load_services(self, artifact_dir: Path) -> None:
|
|
153
|
+
"""Load KuzuIngestor + CypherGenerator + SemanticSearchService from artifact dir."""
|
|
154
|
+
meta_file = artifact_dir / "meta.json"
|
|
155
|
+
if not meta_file.exists():
|
|
156
|
+
raise FileNotFoundError(f"meta.json not found in {artifact_dir}")
|
|
157
|
+
|
|
158
|
+
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
159
|
+
repo_path = Path(meta["repo_path"])
|
|
160
|
+
db_path = artifact_dir / "graph.db"
|
|
161
|
+
vectors_path = artifact_dir / "vectors.pkl"
|
|
162
|
+
|
|
163
|
+
self.close()
|
|
164
|
+
self._active_repo_path = repo_path
|
|
165
|
+
self._active_artifact_dir = artifact_dir
|
|
166
|
+
try:
|
|
167
|
+
self._file_editor = FileEditor(repo_path)
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
logger.warning(f"File editor unavailable: {exc}")
|
|
170
|
+
|
|
171
|
+
ingestor = KuzuIngestor(db_path)
|
|
172
|
+
ingestor.__enter__()
|
|
173
|
+
self._ingestor = ingestor
|
|
174
|
+
|
|
175
|
+
llm = create_llm_backend()
|
|
176
|
+
cypher_gen: CypherGenerator | None = None
|
|
177
|
+
if llm.available:
|
|
178
|
+
cypher_gen = CypherGenerator(llm)
|
|
179
|
+
else:
|
|
180
|
+
logger.warning("LLM not configured — query_code_graph will be unavailable")
|
|
181
|
+
|
|
182
|
+
semantic_service: SemanticSearchService | None = None
|
|
183
|
+
if vectors_path.exists():
|
|
184
|
+
try:
|
|
185
|
+
vector_store = _load_vector_store(vectors_path)
|
|
186
|
+
from ..embeddings.qwen3_embedder import create_embedder
|
|
187
|
+
embedder = create_embedder(batch_size=10)
|
|
188
|
+
semantic_service = SemanticSearchService(
|
|
189
|
+
embedder=embedder,
|
|
190
|
+
vector_store=vector_store,
|
|
191
|
+
graph_service=ingestor,
|
|
192
|
+
)
|
|
193
|
+
logger.info(f"Loaded vector store: {vector_store.get_stats()}")
|
|
194
|
+
except Exception as exc:
|
|
195
|
+
logger.warning(
|
|
196
|
+
f"Semantic search unavailable: {exc}. "
|
|
197
|
+
"Check DASHSCOPE_API_KEY or EMBEDDING_API_KEY / OPENAI_API_KEY."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
self._cypher_gen = cypher_gen
|
|
201
|
+
self._semantic_service = semantic_service
|
|
202
|
+
|
|
203
|
+
def _set_active(self, artifact_dir: Path) -> None:
|
|
204
|
+
"""Mark artifact_dir as active in workspace."""
|
|
205
|
+
(self._workspace / "active.txt").write_text(
|
|
206
|
+
artifact_dir.name, encoding="utf-8"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def close(self) -> None:
|
|
210
|
+
if self._ingestor is not None:
|
|
211
|
+
try:
|
|
212
|
+
self._ingestor.__exit__(None, None, None)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
self._ingestor = None
|
|
216
|
+
self._file_editor = None
|
|
217
|
+
|
|
218
|
+
def _require_active(self) -> None:
|
|
219
|
+
"""Raise :class:`ToolError` when no repository has been indexed."""
|
|
220
|
+
if self._ingestor is None:
|
|
221
|
+
raise ToolError("No repository indexed yet. Call initialize_repository first.")
|
|
222
|
+
|
|
223
|
+
def _require_repo_path(self) -> None:
|
|
224
|
+
"""Raise :class:`ToolError` when no repository path is set."""
|
|
225
|
+
if self._active_repo_path is None:
|
|
226
|
+
raise ToolError("No repository path set. Call initialize_repository first.")
|
|
227
|
+
|
|
228
|
+
def tools(self) -> list[ToolDefinition]:
|
|
229
|
+
defs: list[ToolDefinition] = [
|
|
230
|
+
ToolDefinition(
|
|
231
|
+
name="initialize_repository",
|
|
232
|
+
description=(
|
|
233
|
+
"Index a code repository: builds the knowledge graph, generates vector "
|
|
234
|
+
"embeddings, and produces a multi-page wiki. "
|
|
235
|
+
"Must be called before using any query tools. "
|
|
236
|
+
"Takes 2-10 minutes depending on repo size."
|
|
237
|
+
),
|
|
238
|
+
input_schema={
|
|
239
|
+
"type": "object",
|
|
240
|
+
"properties": {
|
|
241
|
+
"repo_path": {
|
|
242
|
+
"type": "string",
|
|
243
|
+
"description": "Absolute path to the repository to index.",
|
|
244
|
+
},
|
|
245
|
+
"rebuild": {
|
|
246
|
+
"type": "boolean",
|
|
247
|
+
"description": (
|
|
248
|
+
"If true, force-rebuild graph, embeddings, and wiki "
|
|
249
|
+
"even if cached data exists. Default: false."
|
|
250
|
+
),
|
|
251
|
+
},
|
|
252
|
+
"wiki_mode": {
|
|
253
|
+
"type": "string",
|
|
254
|
+
"enum": ["comprehensive", "concise"],
|
|
255
|
+
"description": (
|
|
256
|
+
"comprehensive: 8-10 wiki pages (default). "
|
|
257
|
+
"concise: 4-5 wiki pages."
|
|
258
|
+
),
|
|
259
|
+
},
|
|
260
|
+
"backend": {
|
|
261
|
+
"type": "string",
|
|
262
|
+
"enum": ["kuzu", "memgraph", "memory"],
|
|
263
|
+
"description": (
|
|
264
|
+
"Graph database backend. Default: kuzu (embedded, no Docker)."
|
|
265
|
+
),
|
|
266
|
+
},
|
|
267
|
+
"skip_wiki": {
|
|
268
|
+
"type": "boolean",
|
|
269
|
+
"description": (
|
|
270
|
+
"Skip wiki generation (graph + embeddings only). "
|
|
271
|
+
"Use generate_wiki later to create wiki separately. "
|
|
272
|
+
"Default: false."
|
|
273
|
+
),
|
|
274
|
+
},
|
|
275
|
+
"skip_embed": {
|
|
276
|
+
"type": "boolean",
|
|
277
|
+
"description": (
|
|
278
|
+
"Skip embeddings and wiki (graph only, fastest). "
|
|
279
|
+
"Semantic search will be unavailable. Default: false."
|
|
280
|
+
),
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
"required": ["repo_path"],
|
|
284
|
+
},
|
|
285
|
+
),
|
|
286
|
+
ToolDefinition(
|
|
287
|
+
name="get_repository_info",
|
|
288
|
+
description=(
|
|
289
|
+
"Return information about the currently active (indexed) repository, "
|
|
290
|
+
"including graph statistics (node/relationship counts), wiki pages, "
|
|
291
|
+
"and service availability."
|
|
292
|
+
),
|
|
293
|
+
input_schema={"type": "object", "properties": {}, "required": []},
|
|
294
|
+
),
|
|
295
|
+
ToolDefinition(
|
|
296
|
+
name="list_repositories",
|
|
297
|
+
description=(
|
|
298
|
+
"List all previously indexed repositories in the workspace. "
|
|
299
|
+
"Shows repo name, path, last indexed time, which pipeline steps "
|
|
300
|
+
"have been completed (graph, api_docs, embeddings, wiki), and "
|
|
301
|
+
"which one is currently active. Use this to discover available "
|
|
302
|
+
"repos and switch between them with switch_repository."
|
|
303
|
+
),
|
|
304
|
+
input_schema={"type": "object", "properties": {}, "required": []},
|
|
305
|
+
),
|
|
306
|
+
ToolDefinition(
|
|
307
|
+
name="switch_repository",
|
|
308
|
+
description=(
|
|
309
|
+
"Switch the active repository to a previously indexed one. "
|
|
310
|
+
"After switching, all query tools (query_code_graph, semantic_search, "
|
|
311
|
+
"list_wiki_pages, etc.) will operate on the selected repo. "
|
|
312
|
+
"Use list_repositories first to see available repos."
|
|
313
|
+
),
|
|
314
|
+
input_schema={
|
|
315
|
+
"type": "object",
|
|
316
|
+
"properties": {
|
|
317
|
+
"repo_name": {
|
|
318
|
+
"type": "string",
|
|
319
|
+
"description": (
|
|
320
|
+
"Repository name or artifact directory name "
|
|
321
|
+
"(e.g. 'my-project' or 'my-project_a1b2c3d4'). "
|
|
322
|
+
"Use list_repositories to see available names."
|
|
323
|
+
),
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
"required": ["repo_name"],
|
|
327
|
+
},
|
|
328
|
+
),
|
|
329
|
+
ToolDefinition(
|
|
330
|
+
name="query_code_graph",
|
|
331
|
+
description=(
|
|
332
|
+
"Translate a natural-language question into Cypher and execute it "
|
|
333
|
+
"against the code knowledge graph. Returns raw graph rows."
|
|
334
|
+
),
|
|
335
|
+
input_schema={
|
|
336
|
+
"type": "object",
|
|
337
|
+
"properties": {
|
|
338
|
+
"question": {
|
|
339
|
+
"type": "string",
|
|
340
|
+
"description": "Natural language question about the codebase.",
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
"required": ["question"],
|
|
344
|
+
},
|
|
345
|
+
),
|
|
346
|
+
ToolDefinition(
|
|
347
|
+
name="get_code_snippet",
|
|
348
|
+
description=(
|
|
349
|
+
"Retrieve source code of a function, method, or class by fully qualified name."
|
|
350
|
+
),
|
|
351
|
+
input_schema={
|
|
352
|
+
"type": "object",
|
|
353
|
+
"properties": {
|
|
354
|
+
"qualified_name": {
|
|
355
|
+
"type": "string",
|
|
356
|
+
"description": "Fully qualified name, e.g. 'mymodule.MyClass.my_method'.",
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
"required": ["qualified_name"],
|
|
360
|
+
},
|
|
361
|
+
),
|
|
362
|
+
ToolDefinition(
|
|
363
|
+
name="semantic_search",
|
|
364
|
+
description=(
|
|
365
|
+
"Search the codebase semantically using vector embeddings. "
|
|
366
|
+
"Returns the most relevant functions/classes for the query. "
|
|
367
|
+
"Available after initialize_repository completes."
|
|
368
|
+
),
|
|
369
|
+
input_schema={
|
|
370
|
+
"type": "object",
|
|
371
|
+
"properties": {
|
|
372
|
+
"query": {
|
|
373
|
+
"type": "string",
|
|
374
|
+
"description": "Natural language description of what to find.",
|
|
375
|
+
},
|
|
376
|
+
"top_k": {
|
|
377
|
+
"type": "integer",
|
|
378
|
+
"description": "Number of results. Default: 5.",
|
|
379
|
+
},
|
|
380
|
+
"entity_types": {
|
|
381
|
+
"type": "array",
|
|
382
|
+
"items": {"type": "string"},
|
|
383
|
+
"description": "Filter by type: 'Function', 'Class', 'Method', etc.",
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
"required": ["query"],
|
|
387
|
+
},
|
|
388
|
+
),
|
|
389
|
+
ToolDefinition(
|
|
390
|
+
name="list_wiki_pages",
|
|
391
|
+
description="List all generated wiki pages for the active repository.",
|
|
392
|
+
input_schema={"type": "object", "properties": {}, "required": []},
|
|
393
|
+
),
|
|
394
|
+
ToolDefinition(
|
|
395
|
+
name="get_wiki_page",
|
|
396
|
+
description="Read the content of a generated wiki page.",
|
|
397
|
+
input_schema={
|
|
398
|
+
"type": "object",
|
|
399
|
+
"properties": {
|
|
400
|
+
"page_id": {
|
|
401
|
+
"type": "string",
|
|
402
|
+
"description": (
|
|
403
|
+
"Page ID (e.g. 'page-1') or 'index' for the summary page. "
|
|
404
|
+
"Use list_wiki_pages to see available pages."
|
|
405
|
+
),
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
"required": ["page_id"],
|
|
409
|
+
},
|
|
410
|
+
),
|
|
411
|
+
ToolDefinition(
|
|
412
|
+
name="locate_function",
|
|
413
|
+
description=(
|
|
414
|
+
"Locate a function or method in the repository using Tree-sitter AST. "
|
|
415
|
+
"Returns the source code, start/end line numbers, and qualified name."
|
|
416
|
+
),
|
|
417
|
+
input_schema={
|
|
418
|
+
"type": "object",
|
|
419
|
+
"properties": {
|
|
420
|
+
"file_path": {
|
|
421
|
+
"type": "string",
|
|
422
|
+
"description": "Relative path from repo root.",
|
|
423
|
+
},
|
|
424
|
+
"function_name": {
|
|
425
|
+
"type": "string",
|
|
426
|
+
"description": (
|
|
427
|
+
"Function or method name. "
|
|
428
|
+
"Use 'ClassName.method' to disambiguate overloads."
|
|
429
|
+
),
|
|
430
|
+
},
|
|
431
|
+
"line_number": {
|
|
432
|
+
"type": "integer",
|
|
433
|
+
"description": "Optional: line number to disambiguate overloads.",
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
"required": ["file_path", "function_name"],
|
|
437
|
+
},
|
|
438
|
+
),
|
|
439
|
+
ToolDefinition(
|
|
440
|
+
name="list_api_interfaces",
|
|
441
|
+
description=(
|
|
442
|
+
"List public API interfaces for a module or the entire project. "
|
|
443
|
+
"Returns function signatures, struct/union/enum definitions with "
|
|
444
|
+
"members, typedef declarations, and macro definitions. Particularly "
|
|
445
|
+
"useful for C codebases to understand module boundaries."
|
|
446
|
+
),
|
|
447
|
+
input_schema={
|
|
448
|
+
"type": "object",
|
|
449
|
+
"properties": {
|
|
450
|
+
"module": {
|
|
451
|
+
"type": "string",
|
|
452
|
+
"description": (
|
|
453
|
+
"Module qualified name to query. "
|
|
454
|
+
"If omitted, returns APIs across all modules."
|
|
455
|
+
),
|
|
456
|
+
},
|
|
457
|
+
"visibility": {
|
|
458
|
+
"type": "string",
|
|
459
|
+
"enum": ["public", "static", "extern", "all"],
|
|
460
|
+
"description": (
|
|
461
|
+
"Filter by visibility: 'public' (default) for functions "
|
|
462
|
+
"declared in headers, 'extern' for non-static functions "
|
|
463
|
+
"not in headers, 'static' for file-local functions, "
|
|
464
|
+
"'all' for everything."
|
|
465
|
+
),
|
|
466
|
+
},
|
|
467
|
+
"include_types": {
|
|
468
|
+
"type": "boolean",
|
|
469
|
+
"description": (
|
|
470
|
+
"Include struct/union/enum definitions and typedefs. "
|
|
471
|
+
"Defaults to true."
|
|
472
|
+
),
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
"required": [],
|
|
476
|
+
},
|
|
477
|
+
),
|
|
478
|
+
ToolDefinition(
|
|
479
|
+
name="list_api_docs",
|
|
480
|
+
description=(
|
|
481
|
+
"List available API documentation. Returns the L1 module index "
|
|
482
|
+
"or the L2 module detail page listing all interfaces in that module. "
|
|
483
|
+
"Use this for efficient hierarchical browsing: first list modules, "
|
|
484
|
+
"then drill into a specific module."
|
|
485
|
+
),
|
|
486
|
+
input_schema={
|
|
487
|
+
"type": "object",
|
|
488
|
+
"properties": {
|
|
489
|
+
"module": {
|
|
490
|
+
"type": "string",
|
|
491
|
+
"description": (
|
|
492
|
+
"Module qualified name (e.g. 'project.api'). "
|
|
493
|
+
"If omitted, returns the L1 index listing all modules."
|
|
494
|
+
),
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
"required": [],
|
|
498
|
+
},
|
|
499
|
+
),
|
|
500
|
+
ToolDefinition(
|
|
501
|
+
name="get_api_doc",
|
|
502
|
+
description=(
|
|
503
|
+
"Read the detailed API documentation for a specific function. "
|
|
504
|
+
"Includes signature, docstring, and full call graph "
|
|
505
|
+
"(who calls it and what it calls)."
|
|
506
|
+
),
|
|
507
|
+
input_schema={
|
|
508
|
+
"type": "object",
|
|
509
|
+
"properties": {
|
|
510
|
+
"qualified_name": {
|
|
511
|
+
"type": "string",
|
|
512
|
+
"description": (
|
|
513
|
+
"Fully qualified function name "
|
|
514
|
+
"(e.g. 'project.api.api_init')."
|
|
515
|
+
),
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
"required": ["qualified_name"],
|
|
519
|
+
},
|
|
520
|
+
),
|
|
521
|
+
ToolDefinition(
|
|
522
|
+
name="find_api",
|
|
523
|
+
description=(
|
|
524
|
+
"Find relevant APIs by natural language description. "
|
|
525
|
+
"Combines semantic vector search with API documentation lookup "
|
|
526
|
+
"in a single call — returns matching functions along with their "
|
|
527
|
+
"signatures, docstrings, and call graphs. "
|
|
528
|
+
"Equivalent to running semantic_search + get_api_doc for each result."
|
|
529
|
+
),
|
|
530
|
+
input_schema={
|
|
531
|
+
"type": "object",
|
|
532
|
+
"properties": {
|
|
533
|
+
"query": {
|
|
534
|
+
"type": "string",
|
|
535
|
+
"description": "Natural language description of the API to find.",
|
|
536
|
+
},
|
|
537
|
+
"top_k": {
|
|
538
|
+
"type": "integer",
|
|
539
|
+
"description": "Number of results. Default: 5.",
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
"required": ["query"],
|
|
543
|
+
},
|
|
544
|
+
),
|
|
545
|
+
ToolDefinition(
|
|
546
|
+
name="generate_wiki",
|
|
547
|
+
description=(
|
|
548
|
+
"Regenerate the wiki using existing graph and embeddings. "
|
|
549
|
+
"Use this when wiki generation failed or you want to regenerate "
|
|
550
|
+
"with different settings, without rebuilding the graph or embeddings. "
|
|
551
|
+
"Requires initialize_repository to have been run at least once."
|
|
552
|
+
),
|
|
553
|
+
input_schema={
|
|
554
|
+
"type": "object",
|
|
555
|
+
"properties": {
|
|
556
|
+
"wiki_mode": {
|
|
557
|
+
"type": "string",
|
|
558
|
+
"enum": ["comprehensive", "concise"],
|
|
559
|
+
"description": (
|
|
560
|
+
"comprehensive: 8-10 wiki pages (default). "
|
|
561
|
+
"concise: 4-5 wiki pages."
|
|
562
|
+
),
|
|
563
|
+
},
|
|
564
|
+
"rebuild": {
|
|
565
|
+
"type": "boolean",
|
|
566
|
+
"description": (
|
|
567
|
+
"If true, force-regenerate wiki structure and all pages "
|
|
568
|
+
"even if cached. Default: false (regenerates pages only)."
|
|
569
|
+
),
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
"required": [],
|
|
573
|
+
},
|
|
574
|
+
),
|
|
575
|
+
ToolDefinition(
|
|
576
|
+
name="rebuild_embeddings",
|
|
577
|
+
description=(
|
|
578
|
+
"Rebuild vector embeddings using the existing knowledge graph. "
|
|
579
|
+
"Use this when embeddings are missing, corrupted, or when you "
|
|
580
|
+
"want to re-embed after changing the embedding model/config. "
|
|
581
|
+
"Requires a graph to have been built first "
|
|
582
|
+
"(via initialize_repository or build_graph)."
|
|
583
|
+
),
|
|
584
|
+
input_schema={
|
|
585
|
+
"type": "object",
|
|
586
|
+
"properties": {
|
|
587
|
+
"rebuild": {
|
|
588
|
+
"type": "boolean",
|
|
589
|
+
"description": (
|
|
590
|
+
"If true, force-rebuild embeddings even if cached. "
|
|
591
|
+
"Default: false (reuses cache if available)."
|
|
592
|
+
),
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
"required": [],
|
|
596
|
+
},
|
|
597
|
+
),
|
|
598
|
+
ToolDefinition(
|
|
599
|
+
name="build_graph",
|
|
600
|
+
description=(
|
|
601
|
+
"Build the code knowledge graph from source code using "
|
|
602
|
+
"Tree-sitter AST parsing. This is step 1 of the pipeline. "
|
|
603
|
+
"After building, use generate_api_docs, rebuild_embeddings, "
|
|
604
|
+
"and generate_wiki as separate steps."
|
|
605
|
+
),
|
|
606
|
+
input_schema={
|
|
607
|
+
"type": "object",
|
|
608
|
+
"properties": {
|
|
609
|
+
"repo_path": {
|
|
610
|
+
"type": "string",
|
|
611
|
+
"description": "Absolute path to the repository to index.",
|
|
612
|
+
},
|
|
613
|
+
"rebuild": {
|
|
614
|
+
"type": "boolean",
|
|
615
|
+
"description": (
|
|
616
|
+
"If true, force-rebuild graph even if cached. "
|
|
617
|
+
"Default: false."
|
|
618
|
+
),
|
|
619
|
+
},
|
|
620
|
+
"backend": {
|
|
621
|
+
"type": "string",
|
|
622
|
+
"enum": ["kuzu", "memgraph", "memory"],
|
|
623
|
+
"description": (
|
|
624
|
+
"Graph database backend. Default: kuzu (embedded)."
|
|
625
|
+
),
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
"required": ["repo_path"],
|
|
629
|
+
},
|
|
630
|
+
),
|
|
631
|
+
ToolDefinition(
|
|
632
|
+
name="generate_api_docs",
|
|
633
|
+
description=(
|
|
634
|
+
"Generate hierarchical API documentation from the existing "
|
|
635
|
+
"knowledge graph. Produces L1 module index, L2 per-module pages, "
|
|
636
|
+
"and L3 per-function detail pages with call graphs. "
|
|
637
|
+
"Requires only a graph database — no embeddings or LLM needed. "
|
|
638
|
+
"This is step 2 of the pipeline."
|
|
639
|
+
),
|
|
640
|
+
input_schema={
|
|
641
|
+
"type": "object",
|
|
642
|
+
"properties": {
|
|
643
|
+
"rebuild": {
|
|
644
|
+
"type": "boolean",
|
|
645
|
+
"description": (
|
|
646
|
+
"If true, force-regenerate API docs even if cached. "
|
|
647
|
+
"Default: false."
|
|
648
|
+
),
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
"required": [],
|
|
652
|
+
},
|
|
653
|
+
),
|
|
654
|
+
ToolDefinition(
|
|
655
|
+
name="prepare_guidance",
|
|
656
|
+
description=(
|
|
657
|
+
"Analyze a design document and generate a code generation "
|
|
658
|
+
"guidance file. An internal LLM agent searches the codebase "
|
|
659
|
+
"for relevant APIs, similar implementations, and dependency "
|
|
660
|
+
"relationships, then synthesises a structured guidance "
|
|
661
|
+
"Markdown document for downstream code generation."
|
|
662
|
+
),
|
|
663
|
+
input_schema={
|
|
664
|
+
"type": "object",
|
|
665
|
+
"properties": {
|
|
666
|
+
"design_doc": {
|
|
667
|
+
"type": "string",
|
|
668
|
+
"description": (
|
|
669
|
+
"The design document content (Markdown). "
|
|
670
|
+
"The agent reads this, researches the codebase, "
|
|
671
|
+
"and produces a guidance file."
|
|
672
|
+
),
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
"required": ["design_doc"],
|
|
676
|
+
},
|
|
677
|
+
),
|
|
678
|
+
]
|
|
679
|
+
|
|
680
|
+
return defs
|
|
681
|
+
|
|
682
|
+
def get_handler(self, name: str):
|
|
683
|
+
handlers: dict[str, Any] = {
|
|
684
|
+
"initialize_repository": self._handle_initialize_repository,
|
|
685
|
+
"get_repository_info": self._handle_get_repository_info,
|
|
686
|
+
"list_repositories": self._handle_list_repositories,
|
|
687
|
+
"switch_repository": self._handle_switch_repository,
|
|
688
|
+
"query_code_graph": self._handle_query_code_graph,
|
|
689
|
+
"get_code_snippet": self._handle_get_code_snippet,
|
|
690
|
+
"semantic_search": self._handle_semantic_search,
|
|
691
|
+
"list_wiki_pages": self._handle_list_wiki_pages,
|
|
692
|
+
"get_wiki_page": self._handle_get_wiki_page,
|
|
693
|
+
"locate_function": self._handle_locate_function,
|
|
694
|
+
"list_api_interfaces": self._handle_list_api_interfaces,
|
|
695
|
+
"list_api_docs": self._handle_list_api_docs,
|
|
696
|
+
"get_api_doc": self._handle_get_api_doc,
|
|
697
|
+
"find_api": self._handle_find_api,
|
|
698
|
+
"generate_wiki": self._handle_generate_wiki,
|
|
699
|
+
"rebuild_embeddings": self._handle_rebuild_embeddings,
|
|
700
|
+
"build_graph": self._handle_build_graph,
|
|
701
|
+
"generate_api_docs": self._handle_generate_api_docs,
|
|
702
|
+
"prepare_guidance": self._handle_prepare_guidance,
|
|
703
|
+
}
|
|
704
|
+
return handlers.get(name)
|
|
705
|
+
|
|
706
|
+
# -------------------------------------------------------------------------
|
|
707
|
+
# initialize_repository — runs the full pipeline in a thread pool
|
|
708
|
+
# -------------------------------------------------------------------------
|
|
709
|
+
|
|
710
|
+
async def _handle_initialize_repository(
|
|
711
|
+
self,
|
|
712
|
+
repo_path: str,
|
|
713
|
+
rebuild: bool = False,
|
|
714
|
+
wiki_mode: str = "comprehensive",
|
|
715
|
+
backend: str = "kuzu",
|
|
716
|
+
skip_wiki: bool = False,
|
|
717
|
+
skip_embed: bool = False,
|
|
718
|
+
_progress_cb: ProgressCb = None,
|
|
719
|
+
) -> dict[str, Any]:
|
|
720
|
+
repo = Path(repo_path).resolve()
|
|
721
|
+
if not repo.exists():
|
|
722
|
+
raise ToolError(f"Repository path does not exist: {repo}")
|
|
723
|
+
|
|
724
|
+
loop = asyncio.get_event_loop()
|
|
725
|
+
|
|
726
|
+
def sync_progress(msg: str, pct: float = 0.0) -> None:
|
|
727
|
+
if _progress_cb is not None:
|
|
728
|
+
asyncio.run_coroutine_threadsafe(_progress_cb(msg, pct), loop)
|
|
729
|
+
|
|
730
|
+
result = await loop.run_in_executor(
|
|
731
|
+
None,
|
|
732
|
+
lambda: self._run_pipeline(
|
|
733
|
+
repo, rebuild, wiki_mode, sync_progress,
|
|
734
|
+
backend=backend, skip_wiki=skip_wiki, skip_embed=skip_embed,
|
|
735
|
+
),
|
|
736
|
+
)
|
|
737
|
+
return result
|
|
738
|
+
|
|
739
|
+
def _run_pipeline(
|
|
740
|
+
self,
|
|
741
|
+
repo_path: Path,
|
|
742
|
+
rebuild: bool,
|
|
743
|
+
wiki_mode: str,
|
|
744
|
+
progress_cb: ProgressCb = None,
|
|
745
|
+
backend: str = "kuzu",
|
|
746
|
+
skip_wiki: bool = False,
|
|
747
|
+
skip_embed: bool = False,
|
|
748
|
+
) -> dict[str, Any]:
|
|
749
|
+
"""Synchronous pipeline orchestrator: graph → api_docs → embeddings → wiki.
|
|
750
|
+
|
|
751
|
+
Each step calls the same standalone pipeline functions that the
|
|
752
|
+
individual tool handlers use, so behaviour is identical whether
|
|
753
|
+
invoked from ``initialize_repository`` or step-by-step.
|
|
754
|
+
"""
|
|
755
|
+
from ..examples.generate_wiki import MAX_PAGES_COMPREHENSIVE, MAX_PAGES_CONCISE
|
|
756
|
+
|
|
757
|
+
if skip_embed:
|
|
758
|
+
skip_wiki = True # wiki requires embeddings
|
|
759
|
+
|
|
760
|
+
artifact_dir = artifact_dir_for(self._workspace, repo_path)
|
|
761
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
762
|
+
|
|
763
|
+
db_path = artifact_dir / "graph.db"
|
|
764
|
+
vectors_path = artifact_dir / "vectors.pkl"
|
|
765
|
+
wiki_dir = artifact_dir / "wiki"
|
|
766
|
+
comprehensive = wiki_mode != "concise"
|
|
767
|
+
max_pages = MAX_PAGES_COMPREHENSIVE if comprehensive else MAX_PAGES_CONCISE
|
|
768
|
+
|
|
769
|
+
def _step_progress(step: int, total: int, msg: str, pct: float) -> None:
|
|
770
|
+
if progress_cb:
|
|
771
|
+
progress_cb(f"[Step {step}/{total}] {msg}", pct)
|
|
772
|
+
|
|
773
|
+
total_steps = 4
|
|
774
|
+
if skip_embed:
|
|
775
|
+
total_steps = 2 # graph + api_docs only
|
|
776
|
+
elif skip_wiki:
|
|
777
|
+
total_steps = 3 # graph + api_docs + embeddings
|
|
778
|
+
|
|
779
|
+
try:
|
|
780
|
+
# Step 1: build graph
|
|
781
|
+
builder = build_graph(
|
|
782
|
+
repo_path, db_path, rebuild, progress_cb=lambda msg, pct: _step_progress(1, total_steps, msg, pct),
|
|
783
|
+
backend=backend,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
# Step 2: generate API docs
|
|
787
|
+
generate_api_docs_step(
|
|
788
|
+
builder, artifact_dir, rebuild,
|
|
789
|
+
progress_cb=lambda msg, pct: _step_progress(2, total_steps, msg, pct),
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
# Step 2b: LLM description generation for undocumented functions
|
|
793
|
+
generate_descriptions_step(
|
|
794
|
+
artifact_dir=artifact_dir,
|
|
795
|
+
repo_path=repo_path,
|
|
796
|
+
progress_cb=lambda msg, pct: _step_progress(2, total_steps, msg, pct),
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
page_count = 0
|
|
800
|
+
index_path = wiki_dir / "index.md"
|
|
801
|
+
skipped = []
|
|
802
|
+
|
|
803
|
+
if not skip_embed:
|
|
804
|
+
# Step 3: build embeddings
|
|
805
|
+
vector_store, embedder, func_map = build_vector_index(
|
|
806
|
+
builder, repo_path, vectors_path, rebuild,
|
|
807
|
+
progress_cb=lambda msg, pct: _step_progress(3, total_steps, msg, pct),
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
if not skip_wiki:
|
|
811
|
+
# Step 4: generate wiki
|
|
812
|
+
index_path, page_count = run_wiki_generation(
|
|
813
|
+
builder=builder,
|
|
814
|
+
repo_path=repo_path,
|
|
815
|
+
output_dir=wiki_dir,
|
|
816
|
+
max_pages=max_pages,
|
|
817
|
+
rebuild=rebuild,
|
|
818
|
+
comprehensive=comprehensive,
|
|
819
|
+
vector_store=vector_store,
|
|
820
|
+
embedder=embedder,
|
|
821
|
+
func_map=func_map,
|
|
822
|
+
progress_cb=lambda msg, pct: _step_progress(4, total_steps, msg, pct),
|
|
823
|
+
)
|
|
824
|
+
else:
|
|
825
|
+
skipped.append("wiki")
|
|
826
|
+
_step_progress(4, total_steps, "Wiki generation skipped.", 100.0)
|
|
827
|
+
else:
|
|
828
|
+
skipped.extend(["embed", "wiki"])
|
|
829
|
+
_step_progress(3, total_steps, "Embedding skipped.", 40.0)
|
|
830
|
+
_step_progress(4, total_steps, "Wiki skipped (requires embeddings).", 100.0)
|
|
831
|
+
|
|
832
|
+
save_meta(artifact_dir, repo_path, page_count)
|
|
833
|
+
self._set_active(artifact_dir)
|
|
834
|
+
self._load_services(artifact_dir)
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
"status": "success",
|
|
838
|
+
"repo_path": str(repo_path),
|
|
839
|
+
"artifact_dir": str(artifact_dir),
|
|
840
|
+
"wiki_index": str(index_path),
|
|
841
|
+
"wiki_pages": page_count,
|
|
842
|
+
"skipped": skipped,
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
except Exception as exc:
|
|
846
|
+
logger.exception("Pipeline failed")
|
|
847
|
+
raise ToolError({"error": str(exc), "status": "error"}) from exc
|
|
848
|
+
|
|
849
|
+
# -------------------------------------------------------------------------
|
|
850
|
+
# get_repository_info (merged: active repo metadata + graph statistics)
|
|
851
|
+
# -------------------------------------------------------------------------
|
|
852
|
+
|
|
853
|
+
async def _handle_get_repository_info(self) -> dict[str, Any]:
|
|
854
|
+
if self._active_artifact_dir is None:
|
|
855
|
+
raise ToolError("No active repository. Call initialize_repository first.")
|
|
856
|
+
|
|
857
|
+
meta_file = self._active_artifact_dir / "meta.json"
|
|
858
|
+
meta = json.loads(meta_file.read_text(encoding="utf-8")) if meta_file.exists() else {}
|
|
859
|
+
|
|
860
|
+
wiki_pages = []
|
|
861
|
+
wiki_subdir = self._active_artifact_dir / "wiki" / "wiki"
|
|
862
|
+
if wiki_subdir.exists():
|
|
863
|
+
wiki_pages = [p.stem for p in sorted(wiki_subdir.glob("*.md"))]
|
|
864
|
+
|
|
865
|
+
warnings: list[str] = []
|
|
866
|
+
if self._semantic_service is None:
|
|
867
|
+
warnings.append(
|
|
868
|
+
"Semantic search unavailable — check embedding API keys "
|
|
869
|
+
"(DASHSCOPE_API_KEY or EMBEDDING_API_KEY/OPENAI_API_KEY)."
|
|
870
|
+
)
|
|
871
|
+
if self._cypher_gen is None:
|
|
872
|
+
warnings.append(
|
|
873
|
+
"Cypher query unavailable — set LLM_API_KEY, OPENAI_API_KEY, "
|
|
874
|
+
"or MOONSHOT_API_KEY to enable natural language queries."
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
result: dict[str, Any] = {
|
|
878
|
+
"repo_path": str(self._active_repo_path),
|
|
879
|
+
"artifact_dir": str(self._active_artifact_dir),
|
|
880
|
+
"indexed_at": meta.get("indexed_at"),
|
|
881
|
+
"semantic_search_available": self._semantic_service is not None,
|
|
882
|
+
"cypher_query_available": self._cypher_gen is not None,
|
|
883
|
+
"wiki_pages": wiki_pages,
|
|
884
|
+
}
|
|
885
|
+
if warnings:
|
|
886
|
+
result["warnings"] = warnings
|
|
887
|
+
|
|
888
|
+
# Merge graph statistics + language stats
|
|
889
|
+
if self._ingestor is not None:
|
|
890
|
+
try:
|
|
891
|
+
result["graph_stats"] = self._ingestor.get_statistics()
|
|
892
|
+
except Exception as exc:
|
|
893
|
+
result["graph_stats"] = {"error": str(exc)}
|
|
894
|
+
|
|
895
|
+
# Language extraction stats
|
|
896
|
+
try:
|
|
897
|
+
file_rows = self._ingestor.query(
|
|
898
|
+
"MATCH (f:File) RETURN f.path AS path"
|
|
899
|
+
)
|
|
900
|
+
from ..language_spec import get_language_for_extension
|
|
901
|
+
lang_counts: dict[str, int] = {}
|
|
902
|
+
total_files = 0
|
|
903
|
+
for row in file_rows:
|
|
904
|
+
raw = row.get("result", row)
|
|
905
|
+
fpath = raw[0] if isinstance(raw, (list, tuple)) else raw
|
|
906
|
+
if isinstance(fpath, str):
|
|
907
|
+
ext = Path(fpath).suffix.lower()
|
|
908
|
+
lang = get_language_for_extension(ext)
|
|
909
|
+
if lang:
|
|
910
|
+
lang_counts[lang.value] = lang_counts.get(lang.value, 0) + 1
|
|
911
|
+
total_files += 1
|
|
912
|
+
result["language_stats"] = {
|
|
913
|
+
"total_code_files": total_files,
|
|
914
|
+
"by_language": dict(sorted(lang_counts.items(), key=lambda x: -x[1])),
|
|
915
|
+
}
|
|
916
|
+
except Exception:
|
|
917
|
+
pass # language stats are optional
|
|
918
|
+
|
|
919
|
+
# Supported languages
|
|
920
|
+
from ..constants import LANGUAGE_METADATA, LanguageStatus
|
|
921
|
+
result["supported_languages"] = {
|
|
922
|
+
"full": [m.display_name for _, m in LANGUAGE_METADATA.items() if m.status == LanguageStatus.FULL],
|
|
923
|
+
"in_development": [m.display_name for _, m in LANGUAGE_METADATA.items() if m.status == LanguageStatus.DEV],
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return result
|
|
927
|
+
|
|
928
|
+
# -------------------------------------------------------------------------
|
|
929
|
+
# list_repositories / switch_repository
|
|
930
|
+
# -------------------------------------------------------------------------
|
|
931
|
+
|
|
932
|
+
async def _handle_list_repositories(self) -> dict[str, Any]:
|
|
933
|
+
active_name = None
|
|
934
|
+
active_file = self._workspace / "active.txt"
|
|
935
|
+
if active_file.exists():
|
|
936
|
+
active_name = active_file.read_text(encoding="utf-8").strip()
|
|
937
|
+
|
|
938
|
+
repos: list[dict[str, Any]] = []
|
|
939
|
+
for child in sorted(self._workspace.iterdir()):
|
|
940
|
+
if not child.is_dir():
|
|
941
|
+
continue
|
|
942
|
+
meta_file = child / "meta.json"
|
|
943
|
+
if not meta_file.exists():
|
|
944
|
+
continue
|
|
945
|
+
try:
|
|
946
|
+
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
947
|
+
except (json.JSONDecodeError, OSError):
|
|
948
|
+
continue
|
|
949
|
+
|
|
950
|
+
repos.append({
|
|
951
|
+
"artifact_dir": child.name,
|
|
952
|
+
"repo_name": meta.get("repo_name", child.name),
|
|
953
|
+
"repo_path": meta.get("repo_path", "unknown"),
|
|
954
|
+
"indexed_at": meta.get("indexed_at"),
|
|
955
|
+
"wiki_page_count": meta.get("wiki_page_count", 0),
|
|
956
|
+
"steps": meta.get("steps", {}),
|
|
957
|
+
"active": child.name == active_name,
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
return {
|
|
961
|
+
"workspace": str(self._workspace),
|
|
962
|
+
"repository_count": len(repos),
|
|
963
|
+
"repositories": repos,
|
|
964
|
+
"hint": (
|
|
965
|
+
"Use switch_repository with repo_name to change the active repo. "
|
|
966
|
+
"Use initialize_repository or build_graph to index a new repo."
|
|
967
|
+
),
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async def _handle_switch_repository(self, repo_name: str) -> dict[str, Any]:
|
|
971
|
+
# Try exact match on artifact_dir name first
|
|
972
|
+
target: Path | None = None
|
|
973
|
+
for child in self._workspace.iterdir():
|
|
974
|
+
if not child.is_dir():
|
|
975
|
+
continue
|
|
976
|
+
if child.name == repo_name:
|
|
977
|
+
target = child
|
|
978
|
+
break
|
|
979
|
+
|
|
980
|
+
# Fallback: match by repo_name in meta.json
|
|
981
|
+
if target is None:
|
|
982
|
+
for child in sorted(self._workspace.iterdir()):
|
|
983
|
+
if not child.is_dir():
|
|
984
|
+
continue
|
|
985
|
+
meta_file = child / "meta.json"
|
|
986
|
+
if not meta_file.exists():
|
|
987
|
+
continue
|
|
988
|
+
try:
|
|
989
|
+
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
990
|
+
except (json.JSONDecodeError, OSError):
|
|
991
|
+
continue
|
|
992
|
+
if meta.get("repo_name") == repo_name:
|
|
993
|
+
target = child
|
|
994
|
+
break
|
|
995
|
+
|
|
996
|
+
if target is None or not (target / "meta.json").exists():
|
|
997
|
+
raise ToolError({
|
|
998
|
+
"error": f"Repository not found: {repo_name}",
|
|
999
|
+
"hint": "Use list_repositories to see available repos.",
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
try:
|
|
1003
|
+
self._set_active(target)
|
|
1004
|
+
self._load_services(target)
|
|
1005
|
+
except Exception as exc:
|
|
1006
|
+
raise ToolError({
|
|
1007
|
+
"error": f"Failed to switch: {exc}",
|
|
1008
|
+
"repo_name": repo_name,
|
|
1009
|
+
}) from exc
|
|
1010
|
+
|
|
1011
|
+
meta = json.loads((target / "meta.json").read_text(encoding="utf-8"))
|
|
1012
|
+
return {
|
|
1013
|
+
"status": "success",
|
|
1014
|
+
"active_repo": meta.get("repo_name", target.name),
|
|
1015
|
+
"repo_path": meta.get("repo_path"),
|
|
1016
|
+
"artifact_dir": str(target),
|
|
1017
|
+
"steps": meta.get("steps", {}),
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
# -------------------------------------------------------------------------
|
|
1021
|
+
# query_code_graph
|
|
1022
|
+
# -------------------------------------------------------------------------
|
|
1023
|
+
|
|
1024
|
+
async def _handle_query_code_graph(self, question: str) -> dict[str, Any]:
|
|
1025
|
+
self._require_active()
|
|
1026
|
+
|
|
1027
|
+
if self._cypher_gen is None:
|
|
1028
|
+
raise ToolError(
|
|
1029
|
+
"LLM not configured. Set one of: LLM_API_KEY, OPENAI_API_KEY, "
|
|
1030
|
+
"or MOONSHOT_API_KEY in the MCP server environment."
|
|
1031
|
+
)
|
|
1032
|
+
assert self._ingestor is not None
|
|
1033
|
+
|
|
1034
|
+
try:
|
|
1035
|
+
cypher = self._cypher_gen.generate(question)
|
|
1036
|
+
except Exception as exc:
|
|
1037
|
+
raise ToolError({"error": f"Cypher generation failed: {exc}", "question": question}) from exc
|
|
1038
|
+
|
|
1039
|
+
try:
|
|
1040
|
+
rows = self._ingestor.query(cypher)
|
|
1041
|
+
serialisable = []
|
|
1042
|
+
for row in rows:
|
|
1043
|
+
raw = row.get("result", row)
|
|
1044
|
+
if isinstance(raw, (list, tuple)):
|
|
1045
|
+
serialisable.append(list(raw))
|
|
1046
|
+
else:
|
|
1047
|
+
serialisable.append(raw)
|
|
1048
|
+
return {
|
|
1049
|
+
"question": question,
|
|
1050
|
+
"cypher": cypher,
|
|
1051
|
+
"row_count": len(serialisable),
|
|
1052
|
+
"rows": serialisable,
|
|
1053
|
+
}
|
|
1054
|
+
except Exception as exc:
|
|
1055
|
+
raise ToolError({
|
|
1056
|
+
"error": f"Query execution failed: {exc}",
|
|
1057
|
+
"question": question,
|
|
1058
|
+
"cypher": cypher,
|
|
1059
|
+
}) from exc
|
|
1060
|
+
|
|
1061
|
+
# -------------------------------------------------------------------------
|
|
1062
|
+
# get_code_snippet
|
|
1063
|
+
# -------------------------------------------------------------------------
|
|
1064
|
+
|
|
1065
|
+
async def _handle_get_code_snippet(self, qualified_name: str) -> dict[str, Any]:
|
|
1066
|
+
self._require_active()
|
|
1067
|
+
|
|
1068
|
+
assert self._ingestor is not None
|
|
1069
|
+
|
|
1070
|
+
safe_qn = qualified_name.replace("'", "\\'")
|
|
1071
|
+
cypher = (
|
|
1072
|
+
f"MATCH (n) WHERE n.qualified_name = '{safe_qn}' "
|
|
1073
|
+
"RETURN n.qualified_name, n.name, n.source_code, n.path, n.start_line, n.end_line "
|
|
1074
|
+
"LIMIT 1"
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
try:
|
|
1078
|
+
rows = self._ingestor.query(cypher)
|
|
1079
|
+
except Exception as exc:
|
|
1080
|
+
raise ToolError({"error": f"Graph query failed: {exc}", "qualified_name": qualified_name}) from exc
|
|
1081
|
+
|
|
1082
|
+
if not rows:
|
|
1083
|
+
raise ToolError({"error": "Not found", "qualified_name": qualified_name})
|
|
1084
|
+
|
|
1085
|
+
result = rows[0].get("result", [])
|
|
1086
|
+
qname = result[0] if len(result) > 0 else qualified_name
|
|
1087
|
+
name = result[1] if len(result) > 1 else None
|
|
1088
|
+
source_code = result[2] if len(result) > 2 else None
|
|
1089
|
+
file_path = result[3] if len(result) > 3 else None
|
|
1090
|
+
start_line = result[4] if len(result) > 4 else None
|
|
1091
|
+
end_line = result[5] if len(result) > 5 else None
|
|
1092
|
+
|
|
1093
|
+
if not source_code and file_path and start_line and end_line:
|
|
1094
|
+
fp = Path(str(file_path))
|
|
1095
|
+
if not fp.is_absolute() and self._active_repo_path:
|
|
1096
|
+
fp = self._active_repo_path / fp
|
|
1097
|
+
try:
|
|
1098
|
+
lines = fp.read_text(encoding="utf-8", errors="ignore").splitlines(keepends=True)
|
|
1099
|
+
s = max(0, int(start_line) - 1)
|
|
1100
|
+
e = min(len(lines), int(end_line))
|
|
1101
|
+
source_code = "".join(lines[s:e])
|
|
1102
|
+
except Exception:
|
|
1103
|
+
pass
|
|
1104
|
+
|
|
1105
|
+
return {
|
|
1106
|
+
"qualified_name": qname,
|
|
1107
|
+
"name": name,
|
|
1108
|
+
"file_path": file_path,
|
|
1109
|
+
"start_line": start_line,
|
|
1110
|
+
"end_line": end_line,
|
|
1111
|
+
"source_code": source_code,
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
# -------------------------------------------------------------------------
|
|
1115
|
+
# semantic_search
|
|
1116
|
+
# -------------------------------------------------------------------------
|
|
1117
|
+
|
|
1118
|
+
async def _handle_semantic_search(
|
|
1119
|
+
self,
|
|
1120
|
+
query: str,
|
|
1121
|
+
top_k: int = 5,
|
|
1122
|
+
entity_types: list[str] | None = None,
|
|
1123
|
+
) -> dict[str, Any]:
|
|
1124
|
+
self._require_active()
|
|
1125
|
+
|
|
1126
|
+
if self._semantic_service is None:
|
|
1127
|
+
raise ToolError("Semantic search not available. Re-run initialize_repository to build embeddings.")
|
|
1128
|
+
|
|
1129
|
+
try:
|
|
1130
|
+
results = self._semantic_service.search(query, top_k=top_k, entity_types=entity_types)
|
|
1131
|
+
return {
|
|
1132
|
+
"query": query,
|
|
1133
|
+
"result_count": len(results),
|
|
1134
|
+
"results": [
|
|
1135
|
+
{
|
|
1136
|
+
"qualified_name": r.qualified_name,
|
|
1137
|
+
"name": r.name,
|
|
1138
|
+
"type": r.type,
|
|
1139
|
+
"score": r.score,
|
|
1140
|
+
"file_path": r.file_path,
|
|
1141
|
+
"start_line": r.start_line,
|
|
1142
|
+
"end_line": r.end_line,
|
|
1143
|
+
"source_code": r.source_code,
|
|
1144
|
+
}
|
|
1145
|
+
for r in results
|
|
1146
|
+
],
|
|
1147
|
+
}
|
|
1148
|
+
except Exception as exc:
|
|
1149
|
+
raise ToolError({"error": f"Semantic search failed: {exc}", "query": query}) from exc
|
|
1150
|
+
|
|
1151
|
+
# -------------------------------------------------------------------------
|
|
1152
|
+
# path safety helper (used by locate_function)
|
|
1153
|
+
# -------------------------------------------------------------------------
|
|
1154
|
+
|
|
1155
|
+
def _safe_path(self, rel_path: str) -> Path | None:
|
|
1156
|
+
if self._active_repo_path is None:
|
|
1157
|
+
return None
|
|
1158
|
+
target = (self._active_repo_path / rel_path).resolve()
|
|
1159
|
+
try:
|
|
1160
|
+
target.relative_to(self._active_repo_path.resolve())
|
|
1161
|
+
except ValueError:
|
|
1162
|
+
return None
|
|
1163
|
+
return target
|
|
1164
|
+
|
|
1165
|
+
# -------------------------------------------------------------------------
|
|
1166
|
+
# wiki tools
|
|
1167
|
+
# -------------------------------------------------------------------------
|
|
1168
|
+
|
|
1169
|
+
def _wiki_dir(self) -> Path | None:
|
|
1170
|
+
if self._active_artifact_dir is None:
|
|
1171
|
+
return None
|
|
1172
|
+
return self._active_artifact_dir / "wiki"
|
|
1173
|
+
|
|
1174
|
+
async def _handle_list_wiki_pages(self) -> dict[str, Any]:
|
|
1175
|
+
self._require_active()
|
|
1176
|
+
|
|
1177
|
+
wiki_dir = self._wiki_dir()
|
|
1178
|
+
if wiki_dir is None or not wiki_dir.exists():
|
|
1179
|
+
raise ToolError("Wiki not generated yet. Run initialize_repository first.")
|
|
1180
|
+
|
|
1181
|
+
pages = []
|
|
1182
|
+
wiki_subdir = wiki_dir / "wiki"
|
|
1183
|
+
if wiki_subdir.exists():
|
|
1184
|
+
for p in sorted(wiki_subdir.glob("*.md")):
|
|
1185
|
+
pages.append({"page_id": p.stem, "file": f"wiki/{p.name}"})
|
|
1186
|
+
|
|
1187
|
+
index_path = wiki_dir / "index.md"
|
|
1188
|
+
return {
|
|
1189
|
+
"index_available": index_path.exists(),
|
|
1190
|
+
"page_count": len(pages),
|
|
1191
|
+
"pages": pages,
|
|
1192
|
+
"hint": "Use get_wiki_page with page_id='index' for the summary, or a specific page-N id.",
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
async def _handle_get_wiki_page(self, page_id: str) -> dict[str, Any]:
|
|
1196
|
+
self._require_active()
|
|
1197
|
+
|
|
1198
|
+
wiki_dir = self._wiki_dir()
|
|
1199
|
+
if wiki_dir is None or not wiki_dir.exists():
|
|
1200
|
+
raise ToolError("Wiki not generated yet. Run initialize_repository first.")
|
|
1201
|
+
|
|
1202
|
+
if page_id == "index":
|
|
1203
|
+
target = wiki_dir / "index.md"
|
|
1204
|
+
else:
|
|
1205
|
+
target = wiki_dir / "wiki" / f"{page_id}.md"
|
|
1206
|
+
|
|
1207
|
+
if not target.exists():
|
|
1208
|
+
raise ToolError({"error": f"Wiki page not found: {page_id}", "page_id": page_id})
|
|
1209
|
+
|
|
1210
|
+
content = target.read_text(encoding="utf-8", errors="ignore")
|
|
1211
|
+
return {
|
|
1212
|
+
"page_id": page_id,
|
|
1213
|
+
"file_path": str(target),
|
|
1214
|
+
"content": content,
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
# -------------------------------------------------------------------------
|
|
1218
|
+
# locate_function
|
|
1219
|
+
# -------------------------------------------------------------------------
|
|
1220
|
+
|
|
1221
|
+
async def _handle_locate_function(
|
|
1222
|
+
self,
|
|
1223
|
+
file_path: str,
|
|
1224
|
+
function_name: str,
|
|
1225
|
+
line_number: int | None = None,
|
|
1226
|
+
) -> dict[str, Any]:
|
|
1227
|
+
self._require_repo_path()
|
|
1228
|
+
if self._file_editor is None:
|
|
1229
|
+
raise ToolError("File editor not initialized.")
|
|
1230
|
+
|
|
1231
|
+
target = self._safe_path(file_path)
|
|
1232
|
+
if target is None:
|
|
1233
|
+
raise ToolError({"error": "Path outside repository root.", "file_path": file_path})
|
|
1234
|
+
if not target.exists():
|
|
1235
|
+
raise ToolError({"error": "File not found.", "file_path": file_path})
|
|
1236
|
+
|
|
1237
|
+
result = self._file_editor.locate_function(target, function_name, line_number)
|
|
1238
|
+
if result is None:
|
|
1239
|
+
raise ToolError({
|
|
1240
|
+
"error": f"Function '{function_name}' not found in {file_path}.",
|
|
1241
|
+
"file_path": file_path,
|
|
1242
|
+
"function_name": function_name,
|
|
1243
|
+
})
|
|
1244
|
+
return result
|
|
1245
|
+
|
|
1246
|
+
# -------------------------------------------------------------------------
|
|
1247
|
+
# list_api_interfaces
|
|
1248
|
+
# -------------------------------------------------------------------------
|
|
1249
|
+
|
|
1250
|
+
async def _handle_list_api_interfaces(
|
|
1251
|
+
self,
|
|
1252
|
+
module: str | None = None,
|
|
1253
|
+
visibility: str = "public",
|
|
1254
|
+
include_types: bool = True,
|
|
1255
|
+
) -> dict[str, Any]:
|
|
1256
|
+
self._require_active()
|
|
1257
|
+
|
|
1258
|
+
assert self._ingestor is not None
|
|
1259
|
+
|
|
1260
|
+
vis_filter = None if visibility == "all" else visibility
|
|
1261
|
+
|
|
1262
|
+
try:
|
|
1263
|
+
rows = self._ingestor.fetch_module_apis(
|
|
1264
|
+
module_qn=module,
|
|
1265
|
+
visibility=vis_filter,
|
|
1266
|
+
)
|
|
1267
|
+
|
|
1268
|
+
# Group function results by module
|
|
1269
|
+
by_module: dict[str, list[dict[str, Any]]] = {}
|
|
1270
|
+
for row in rows:
|
|
1271
|
+
raw = row.get("result", row)
|
|
1272
|
+
if isinstance(raw, (list, tuple)) and len(raw) >= 8:
|
|
1273
|
+
mod_name = raw[0] or "unknown"
|
|
1274
|
+
entry: dict[str, Any] = {
|
|
1275
|
+
"name": raw[1],
|
|
1276
|
+
"signature": raw[2],
|
|
1277
|
+
"return_type": raw[3],
|
|
1278
|
+
"visibility": raw[4],
|
|
1279
|
+
"parameters": raw[5],
|
|
1280
|
+
"start_line": raw[6],
|
|
1281
|
+
"end_line": raw[7],
|
|
1282
|
+
"entity_type": "function",
|
|
1283
|
+
}
|
|
1284
|
+
else:
|
|
1285
|
+
mod_name = raw.get("module", "unknown") if isinstance(raw, dict) else "unknown"
|
|
1286
|
+
entry = raw if isinstance(raw, dict) else {"raw": raw}
|
|
1287
|
+
if isinstance(entry, dict):
|
|
1288
|
+
entry["entity_type"] = "function"
|
|
1289
|
+
|
|
1290
|
+
if mod_name not in by_module:
|
|
1291
|
+
by_module[mod_name] = []
|
|
1292
|
+
by_module[mod_name].append(entry)
|
|
1293
|
+
|
|
1294
|
+
# Fetch type APIs (structs, unions, enums, typedefs) if requested
|
|
1295
|
+
type_count = 0
|
|
1296
|
+
if include_types and hasattr(self._ingestor, "fetch_module_type_apis"):
|
|
1297
|
+
type_rows = self._ingestor.fetch_module_type_apis(module_qn=module)
|
|
1298
|
+
for row in type_rows:
|
|
1299
|
+
raw = row.get("result", row)
|
|
1300
|
+
if isinstance(raw, (list, tuple)) and len(raw) >= 6:
|
|
1301
|
+
mod_name = raw[0] or "unknown"
|
|
1302
|
+
entry = {
|
|
1303
|
+
"name": raw[1],
|
|
1304
|
+
"kind": raw[2],
|
|
1305
|
+
"signature": raw[3],
|
|
1306
|
+
"members": raw[4] if len(raw) > 4 else None,
|
|
1307
|
+
"start_line": raw[4 if len(raw) <= 5 else 5],
|
|
1308
|
+
"end_line": raw[5 if len(raw) <= 6 else 6],
|
|
1309
|
+
"entity_type": raw[2] or "type",
|
|
1310
|
+
}
|
|
1311
|
+
else:
|
|
1312
|
+
mod_name = raw.get("module", "unknown") if isinstance(raw, dict) else "unknown"
|
|
1313
|
+
entry = raw if isinstance(raw, dict) else {"raw": raw}
|
|
1314
|
+
|
|
1315
|
+
if mod_name not in by_module:
|
|
1316
|
+
by_module[mod_name] = []
|
|
1317
|
+
by_module[mod_name].append(entry)
|
|
1318
|
+
type_count += 1
|
|
1319
|
+
|
|
1320
|
+
total = sum(len(v) for v in by_module.values())
|
|
1321
|
+
return {
|
|
1322
|
+
"total_apis": total,
|
|
1323
|
+
"function_count": total - type_count,
|
|
1324
|
+
"type_count": type_count,
|
|
1325
|
+
"module_count": len(by_module),
|
|
1326
|
+
"visibility_filter": visibility,
|
|
1327
|
+
"modules": by_module,
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
except Exception as exc:
|
|
1331
|
+
raise ToolError(f"Failed to list API interfaces: {exc}") from exc
|
|
1332
|
+
|
|
1333
|
+
# -------------------------------------------------------------------------
|
|
1334
|
+
# list_api_docs / get_api_doc (hierarchical API documentation)
|
|
1335
|
+
# -------------------------------------------------------------------------
|
|
1336
|
+
|
|
1337
|
+
def _api_docs_dir(self) -> Path | None:
|
|
1338
|
+
if self._active_artifact_dir is None:
|
|
1339
|
+
return None
|
|
1340
|
+
return self._active_artifact_dir / "api_docs"
|
|
1341
|
+
|
|
1342
|
+
async def _handle_list_api_docs(
|
|
1343
|
+
self,
|
|
1344
|
+
module: str | None = None,
|
|
1345
|
+
) -> dict[str, Any]:
|
|
1346
|
+
self._require_active()
|
|
1347
|
+
|
|
1348
|
+
api_dir = self._api_docs_dir()
|
|
1349
|
+
if api_dir is None or not (api_dir / "index.md").exists():
|
|
1350
|
+
raise ToolError(
|
|
1351
|
+
"API docs not generated yet. "
|
|
1352
|
+
"Re-run initialize_repository to generate them."
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
if module:
|
|
1356
|
+
# L2: module detail page
|
|
1357
|
+
safe = module.replace("/", "_").replace("\\", "_")
|
|
1358
|
+
target = api_dir / "modules" / f"{safe}.md"
|
|
1359
|
+
if not target.exists():
|
|
1360
|
+
raise ToolError({
|
|
1361
|
+
"error": f"Module doc not found: {module}",
|
|
1362
|
+
"module": module,
|
|
1363
|
+
"hint": "Use list_api_docs (no args) to see available modules.",
|
|
1364
|
+
})
|
|
1365
|
+
return {
|
|
1366
|
+
"level": "module",
|
|
1367
|
+
"module": module,
|
|
1368
|
+
"content": target.read_text(encoding="utf-8", errors="ignore"),
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
# L1: global index
|
|
1372
|
+
index_path = api_dir / "index.md"
|
|
1373
|
+
return {
|
|
1374
|
+
"level": "index",
|
|
1375
|
+
"content": index_path.read_text(encoding="utf-8", errors="ignore"),
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
async def _handle_get_api_doc(
|
|
1379
|
+
self,
|
|
1380
|
+
qualified_name: str,
|
|
1381
|
+
) -> dict[str, Any]:
|
|
1382
|
+
self._require_active()
|
|
1383
|
+
|
|
1384
|
+
api_dir = self._api_docs_dir()
|
|
1385
|
+
if api_dir is None or not (api_dir / "index.md").exists():
|
|
1386
|
+
raise ToolError(
|
|
1387
|
+
"API docs not generated yet. "
|
|
1388
|
+
"Re-run initialize_repository to generate them."
|
|
1389
|
+
)
|
|
1390
|
+
|
|
1391
|
+
safe = qualified_name.replace("/", "_").replace("\\", "_")
|
|
1392
|
+
target = api_dir / "funcs" / f"{safe}.md"
|
|
1393
|
+
if not target.exists():
|
|
1394
|
+
raise ToolError({
|
|
1395
|
+
"error": f"API doc not found: {qualified_name}",
|
|
1396
|
+
"qualified_name": qualified_name,
|
|
1397
|
+
"hint": "Use list_api_docs to browse modules first.",
|
|
1398
|
+
})
|
|
1399
|
+
|
|
1400
|
+
return {
|
|
1401
|
+
"qualified_name": qualified_name,
|
|
1402
|
+
"content": target.read_text(encoding="utf-8", errors="ignore"),
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
# -------------------------------------------------------------------------
|
|
1406
|
+
# find_api (aggregated: semantic search + API doc lookup)
|
|
1407
|
+
# -------------------------------------------------------------------------
|
|
1408
|
+
|
|
1409
|
+
async def _handle_find_api(
|
|
1410
|
+
self,
|
|
1411
|
+
query: str,
|
|
1412
|
+
top_k: int = 5,
|
|
1413
|
+
) -> dict[str, Any]:
|
|
1414
|
+
self._require_active()
|
|
1415
|
+
|
|
1416
|
+
if self._semantic_service is None:
|
|
1417
|
+
raise ToolError(
|
|
1418
|
+
"Semantic search not available. "
|
|
1419
|
+
"Re-run initialize_repository to build embeddings."
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
try:
|
|
1423
|
+
results = self._semantic_service.search(query, top_k=top_k)
|
|
1424
|
+
except Exception as exc:
|
|
1425
|
+
raise ToolError(
|
|
1426
|
+
{"error": f"Semantic search failed: {exc}", "query": query}
|
|
1427
|
+
) from exc
|
|
1428
|
+
|
|
1429
|
+
api_dir = self._api_docs_dir()
|
|
1430
|
+
funcs_dir = api_dir / "funcs" if api_dir else None
|
|
1431
|
+
has_api_docs = funcs_dir is not None and funcs_dir.exists()
|
|
1432
|
+
|
|
1433
|
+
combined = []
|
|
1434
|
+
for r in results:
|
|
1435
|
+
entry: dict[str, Any] = {
|
|
1436
|
+
"qualified_name": r.qualified_name,
|
|
1437
|
+
"name": r.name,
|
|
1438
|
+
"type": r.type,
|
|
1439
|
+
"score": r.score,
|
|
1440
|
+
"file_path": r.file_path,
|
|
1441
|
+
"start_line": r.start_line,
|
|
1442
|
+
"end_line": r.end_line,
|
|
1443
|
+
"source_code": r.source_code,
|
|
1444
|
+
"api_doc": None,
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if has_api_docs and r.qualified_name:
|
|
1448
|
+
safe_qn = r.qualified_name.replace("/", "_").replace("\\", "_")
|
|
1449
|
+
doc_file = funcs_dir / f"{safe_qn}.md"
|
|
1450
|
+
if doc_file.exists():
|
|
1451
|
+
entry["api_doc"] = doc_file.read_text(
|
|
1452
|
+
encoding="utf-8", errors="ignore"
|
|
1453
|
+
)
|
|
1454
|
+
|
|
1455
|
+
combined.append(entry)
|
|
1456
|
+
|
|
1457
|
+
return {
|
|
1458
|
+
"query": query,
|
|
1459
|
+
"result_count": len(combined),
|
|
1460
|
+
"api_docs_available": has_api_docs,
|
|
1461
|
+
"results": combined,
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
# -------------------------------------------------------------------------
|
|
1465
|
+
# generate_wiki (standalone wiki regeneration)
|
|
1466
|
+
# -------------------------------------------------------------------------
|
|
1467
|
+
|
|
1468
|
+
async def _handle_generate_wiki(
|
|
1469
|
+
self,
|
|
1470
|
+
wiki_mode: str = "comprehensive",
|
|
1471
|
+
rebuild: bool = False,
|
|
1472
|
+
_progress_cb: ProgressCb = None,
|
|
1473
|
+
) -> dict[str, Any]:
|
|
1474
|
+
self._require_active()
|
|
1475
|
+
|
|
1476
|
+
if self._active_artifact_dir is None or self._active_repo_path is None:
|
|
1477
|
+
raise ToolError("No active repository. Call initialize_repository first.")
|
|
1478
|
+
|
|
1479
|
+
artifact_dir = self._active_artifact_dir
|
|
1480
|
+
repo_path = self._active_repo_path
|
|
1481
|
+
vectors_path = artifact_dir / "vectors.pkl"
|
|
1482
|
+
|
|
1483
|
+
if not vectors_path.exists():
|
|
1484
|
+
raise ToolError(
|
|
1485
|
+
"Embeddings not found. Run initialize_repository first "
|
|
1486
|
+
"to build the graph and embeddings."
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
loop = asyncio.get_event_loop()
|
|
1490
|
+
|
|
1491
|
+
def sync_progress(msg: str, pct: float = 0.0) -> None:
|
|
1492
|
+
if _progress_cb is not None:
|
|
1493
|
+
asyncio.run_coroutine_threadsafe(_progress_cb(msg, pct), loop)
|
|
1494
|
+
|
|
1495
|
+
result = await loop.run_in_executor(
|
|
1496
|
+
None,
|
|
1497
|
+
lambda: self._run_wiki_generation(
|
|
1498
|
+
repo_path, artifact_dir, vectors_path,
|
|
1499
|
+
wiki_mode, rebuild, sync_progress,
|
|
1500
|
+
),
|
|
1501
|
+
)
|
|
1502
|
+
return result
|
|
1503
|
+
|
|
1504
|
+
def _run_wiki_generation(
|
|
1505
|
+
self,
|
|
1506
|
+
repo_path: Path,
|
|
1507
|
+
artifact_dir: Path,
|
|
1508
|
+
vectors_path: Path,
|
|
1509
|
+
wiki_mode: str,
|
|
1510
|
+
rebuild: bool,
|
|
1511
|
+
progress_cb: ProgressCb = None,
|
|
1512
|
+
) -> dict[str, Any]:
|
|
1513
|
+
"""Synchronous wiki generation using existing graph + embeddings."""
|
|
1514
|
+
from ..examples.generate_wiki import MAX_PAGES_COMPREHENSIVE, MAX_PAGES_CONCISE
|
|
1515
|
+
|
|
1516
|
+
comprehensive = wiki_mode != "concise"
|
|
1517
|
+
max_pages = MAX_PAGES_COMPREHENSIVE if comprehensive else MAX_PAGES_CONCISE
|
|
1518
|
+
wiki_dir = artifact_dir / "wiki"
|
|
1519
|
+
|
|
1520
|
+
try:
|
|
1521
|
+
# Load existing embeddings
|
|
1522
|
+
with open(vectors_path, "rb") as fh:
|
|
1523
|
+
cache = pickle.load(fh)
|
|
1524
|
+
vector_store = cache["vector_store"]
|
|
1525
|
+
func_map: dict[int, dict] = cache["func_map"]
|
|
1526
|
+
from ..embeddings.qwen3_embedder import create_embedder
|
|
1527
|
+
embedder = create_embedder()
|
|
1528
|
+
|
|
1529
|
+
# Delete structure cache if rebuild
|
|
1530
|
+
structure_cache = wiki_dir / f"{repo_path.name}_structure.pkl"
|
|
1531
|
+
if rebuild and structure_cache.exists():
|
|
1532
|
+
structure_cache.unlink()
|
|
1533
|
+
|
|
1534
|
+
assert self._ingestor is not None
|
|
1535
|
+
|
|
1536
|
+
index_path, page_count = run_wiki_generation(
|
|
1537
|
+
builder=self._ingestor,
|
|
1538
|
+
repo_path=repo_path,
|
|
1539
|
+
output_dir=wiki_dir,
|
|
1540
|
+
max_pages=max_pages,
|
|
1541
|
+
rebuild=rebuild,
|
|
1542
|
+
comprehensive=comprehensive,
|
|
1543
|
+
vector_store=vector_store,
|
|
1544
|
+
embedder=embedder,
|
|
1545
|
+
func_map=func_map,
|
|
1546
|
+
progress_cb=progress_cb,
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
save_meta(artifact_dir, repo_path, page_count)
|
|
1550
|
+
|
|
1551
|
+
return {
|
|
1552
|
+
"status": "success",
|
|
1553
|
+
"repo_path": str(repo_path),
|
|
1554
|
+
"wiki_index": str(index_path),
|
|
1555
|
+
"wiki_pages": page_count,
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
except Exception as exc:
|
|
1559
|
+
logger.exception("Wiki generation failed")
|
|
1560
|
+
raise ToolError({"error": str(exc), "status": "error"}) from exc
|
|
1561
|
+
|
|
1562
|
+
# -------------------------------------------------------------------------
|
|
1563
|
+
# rebuild_embeddings (standalone embedding rebuild)
|
|
1564
|
+
# -------------------------------------------------------------------------
|
|
1565
|
+
|
|
1566
|
+
async def _handle_rebuild_embeddings(
|
|
1567
|
+
self,
|
|
1568
|
+
rebuild: bool = False,
|
|
1569
|
+
_progress_cb: ProgressCb = None,
|
|
1570
|
+
) -> dict[str, Any]:
|
|
1571
|
+
self._require_active()
|
|
1572
|
+
|
|
1573
|
+
if self._active_artifact_dir is None or self._active_repo_path is None:
|
|
1574
|
+
raise ToolError("No active repository. Call initialize_repository first.")
|
|
1575
|
+
|
|
1576
|
+
artifact_dir = self._active_artifact_dir
|
|
1577
|
+
repo_path = self._active_repo_path
|
|
1578
|
+
db_path = artifact_dir / "graph.db"
|
|
1579
|
+
|
|
1580
|
+
if not db_path.exists():
|
|
1581
|
+
raise ToolError(
|
|
1582
|
+
"Graph database not found. Run initialize_repository first "
|
|
1583
|
+
"to build the knowledge graph."
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
loop = asyncio.get_event_loop()
|
|
1587
|
+
|
|
1588
|
+
def sync_progress(msg: str, pct: float = 0.0) -> None:
|
|
1589
|
+
if _progress_cb is not None:
|
|
1590
|
+
asyncio.run_coroutine_threadsafe(_progress_cb(msg, pct), loop)
|
|
1591
|
+
|
|
1592
|
+
result = await loop.run_in_executor(
|
|
1593
|
+
None,
|
|
1594
|
+
lambda: self._run_rebuild_embeddings(
|
|
1595
|
+
repo_path, artifact_dir, rebuild, sync_progress,
|
|
1596
|
+
),
|
|
1597
|
+
)
|
|
1598
|
+
|
|
1599
|
+
# Reload services so semantic search picks up new embeddings
|
|
1600
|
+
self._load_services(artifact_dir)
|
|
1601
|
+
|
|
1602
|
+
return result
|
|
1603
|
+
|
|
1604
|
+
def _run_rebuild_embeddings(
|
|
1605
|
+
self,
|
|
1606
|
+
repo_path: Path,
|
|
1607
|
+
artifact_dir: Path,
|
|
1608
|
+
rebuild: bool,
|
|
1609
|
+
progress_cb: ProgressCb = None,
|
|
1610
|
+
) -> dict[str, Any]:
|
|
1611
|
+
"""Synchronous embedding rebuild using existing graph."""
|
|
1612
|
+
vectors_path = artifact_dir / "vectors.pkl"
|
|
1613
|
+
|
|
1614
|
+
try:
|
|
1615
|
+
assert self._ingestor is not None
|
|
1616
|
+
|
|
1617
|
+
vector_store, embedder, func_map = build_vector_index(
|
|
1618
|
+
self._ingestor, repo_path, vectors_path, rebuild, progress_cb
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
# Preserve existing wiki_page_count in meta
|
|
1622
|
+
meta_file = artifact_dir / "meta.json"
|
|
1623
|
+
page_count = 0
|
|
1624
|
+
if meta_file.exists():
|
|
1625
|
+
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
1626
|
+
page_count = meta.get("wiki_page_count", 0)
|
|
1627
|
+
|
|
1628
|
+
save_meta(artifact_dir, repo_path, page_count)
|
|
1629
|
+
|
|
1630
|
+
return {
|
|
1631
|
+
"status": "success",
|
|
1632
|
+
"repo_path": str(repo_path),
|
|
1633
|
+
"vectors_path": str(vectors_path),
|
|
1634
|
+
"embedding_count": len(vector_store),
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
except Exception as exc:
|
|
1638
|
+
logger.exception("Embedding rebuild failed")
|
|
1639
|
+
raise ToolError({"error": str(exc), "status": "error"}) from exc
|
|
1640
|
+
|
|
1641
|
+
# -------------------------------------------------------------------------
|
|
1642
|
+
# build_graph (standalone graph build)
|
|
1643
|
+
# -------------------------------------------------------------------------
|
|
1644
|
+
|
|
1645
|
+
async def _handle_build_graph(
|
|
1646
|
+
self,
|
|
1647
|
+
repo_path: str,
|
|
1648
|
+
rebuild: bool = False,
|
|
1649
|
+
backend: str = "kuzu",
|
|
1650
|
+
_progress_cb: ProgressCb = None,
|
|
1651
|
+
) -> dict[str, Any]:
|
|
1652
|
+
repo = Path(repo_path).resolve()
|
|
1653
|
+
if not repo.exists():
|
|
1654
|
+
raise ToolError(f"Repository path does not exist: {repo}")
|
|
1655
|
+
|
|
1656
|
+
loop = asyncio.get_event_loop()
|
|
1657
|
+
|
|
1658
|
+
def sync_progress(msg: str, pct: float = 0.0) -> None:
|
|
1659
|
+
if _progress_cb is not None:
|
|
1660
|
+
asyncio.run_coroutine_threadsafe(_progress_cb(msg, pct), loop)
|
|
1661
|
+
|
|
1662
|
+
result = await loop.run_in_executor(
|
|
1663
|
+
None,
|
|
1664
|
+
lambda: self._run_build_graph(repo, rebuild, backend, sync_progress),
|
|
1665
|
+
)
|
|
1666
|
+
return result
|
|
1667
|
+
|
|
1668
|
+
def _run_build_graph(
|
|
1669
|
+
self,
|
|
1670
|
+
repo_path: Path,
|
|
1671
|
+
rebuild: bool,
|
|
1672
|
+
backend: str,
|
|
1673
|
+
progress_cb: ProgressCb = None,
|
|
1674
|
+
) -> dict[str, Any]:
|
|
1675
|
+
"""Synchronous graph build. Runs in thread pool."""
|
|
1676
|
+
artifact_dir = artifact_dir_for(self._workspace, repo_path)
|
|
1677
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
1678
|
+
db_path = artifact_dir / "graph.db"
|
|
1679
|
+
|
|
1680
|
+
try:
|
|
1681
|
+
builder = build_graph(
|
|
1682
|
+
repo_path, db_path, rebuild, progress_cb, backend=backend,
|
|
1683
|
+
)
|
|
1684
|
+
|
|
1685
|
+
stats = builder.get_statistics()
|
|
1686
|
+
save_meta(artifact_dir, repo_path, 0)
|
|
1687
|
+
self._set_active(artifact_dir)
|
|
1688
|
+
self._load_services(artifact_dir)
|
|
1689
|
+
|
|
1690
|
+
return {
|
|
1691
|
+
"status": "success",
|
|
1692
|
+
"repo_path": str(repo_path),
|
|
1693
|
+
"artifact_dir": str(artifact_dir),
|
|
1694
|
+
"node_count": stats.get("node_count", 0),
|
|
1695
|
+
"relationship_count": stats.get("relationship_count", 0),
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
except Exception as exc:
|
|
1699
|
+
logger.exception("Graph build failed")
|
|
1700
|
+
raise ToolError({"error": str(exc), "status": "error"}) from exc
|
|
1701
|
+
|
|
1702
|
+
# -------------------------------------------------------------------------
|
|
1703
|
+
# generate_api_docs (standalone API doc generation)
|
|
1704
|
+
# -------------------------------------------------------------------------
|
|
1705
|
+
|
|
1706
|
+
async def _handle_generate_api_docs(
|
|
1707
|
+
self,
|
|
1708
|
+
rebuild: bool = False,
|
|
1709
|
+
_progress_cb: ProgressCb = None,
|
|
1710
|
+
) -> dict[str, Any]:
|
|
1711
|
+
self._require_active()
|
|
1712
|
+
|
|
1713
|
+
if self._active_artifact_dir is None or self._active_repo_path is None:
|
|
1714
|
+
raise ToolError("No active repository. Call build_graph or initialize_repository first.")
|
|
1715
|
+
|
|
1716
|
+
artifact_dir = self._active_artifact_dir
|
|
1717
|
+
|
|
1718
|
+
loop = asyncio.get_event_loop()
|
|
1719
|
+
|
|
1720
|
+
def sync_progress(msg: str, pct: float = 0.0) -> None:
|
|
1721
|
+
if _progress_cb is not None:
|
|
1722
|
+
asyncio.run_coroutine_threadsafe(_progress_cb(msg, pct), loop)
|
|
1723
|
+
|
|
1724
|
+
repo_path = self._active_repo_path
|
|
1725
|
+
result = await loop.run_in_executor(
|
|
1726
|
+
None,
|
|
1727
|
+
lambda: self._run_generate_api_docs(artifact_dir, repo_path, rebuild, sync_progress),
|
|
1728
|
+
)
|
|
1729
|
+
return result
|
|
1730
|
+
|
|
1731
|
+
def _run_generate_api_docs(
|
|
1732
|
+
self,
|
|
1733
|
+
artifact_dir: Path,
|
|
1734
|
+
repo_path: Path | None,
|
|
1735
|
+
rebuild: bool,
|
|
1736
|
+
progress_cb: ProgressCb = None,
|
|
1737
|
+
) -> dict[str, Any]:
|
|
1738
|
+
"""Synchronous API docs generation from existing graph."""
|
|
1739
|
+
try:
|
|
1740
|
+
assert self._ingestor is not None
|
|
1741
|
+
|
|
1742
|
+
result = generate_api_docs_step(
|
|
1743
|
+
self._ingestor, artifact_dir, rebuild, progress_cb,
|
|
1744
|
+
)
|
|
1745
|
+
|
|
1746
|
+
# LLM description generation for undocumented functions
|
|
1747
|
+
if repo_path is not None:
|
|
1748
|
+
desc_stats = generate_descriptions_step(
|
|
1749
|
+
artifact_dir=artifact_dir,
|
|
1750
|
+
repo_path=repo_path,
|
|
1751
|
+
progress_cb=progress_cb,
|
|
1752
|
+
)
|
|
1753
|
+
result["desc_stats"] = desc_stats
|
|
1754
|
+
|
|
1755
|
+
return {
|
|
1756
|
+
"status": result.get("status", "success"),
|
|
1757
|
+
"artifact_dir": str(artifact_dir),
|
|
1758
|
+
**{k: v for k, v in result.items() if k != "status"},
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
except Exception as exc:
|
|
1762
|
+
logger.exception("API docs generation failed")
|
|
1763
|
+
raise ToolError({"error": str(exc), "status": "error"}) from exc
|
|
1764
|
+
|
|
1765
|
+
# -------------------------------------------------------------------------
|
|
1766
|
+
# prepare_guidance
|
|
1767
|
+
# -------------------------------------------------------------------------
|
|
1768
|
+
|
|
1769
|
+
async def _handle_prepare_guidance(
|
|
1770
|
+
self,
|
|
1771
|
+
design_doc: str,
|
|
1772
|
+
) -> dict[str, Any]:
|
|
1773
|
+
"""Run the internal GuidanceAgent to produce a code generation guidance file."""
|
|
1774
|
+
self._require_active()
|
|
1775
|
+
|
|
1776
|
+
llm = create_llm_backend()
|
|
1777
|
+
if not llm.available:
|
|
1778
|
+
raise ToolError(
|
|
1779
|
+
"LLM not configured. Set one of: LLM_API_KEY, OPENAI_API_KEY, "
|
|
1780
|
+
"or MOONSHOT_API_KEY to use prepare_guidance."
|
|
1781
|
+
)
|
|
1782
|
+
|
|
1783
|
+
from ..guidance.agent import GuidanceAgent
|
|
1784
|
+
from ..guidance.toolset import MCPToolSet
|
|
1785
|
+
|
|
1786
|
+
tool_set = MCPToolSet(
|
|
1787
|
+
semantic_service=self._semantic_service,
|
|
1788
|
+
cypher_gen=self._cypher_gen,
|
|
1789
|
+
ingestor=self._ingestor,
|
|
1790
|
+
artifact_dir=self._active_artifact_dir,
|
|
1791
|
+
)
|
|
1792
|
+
agent = GuidanceAgent(toolset=tool_set, llm=llm)
|
|
1793
|
+
|
|
1794
|
+
try:
|
|
1795
|
+
guidance = await agent.run(design_doc)
|
|
1796
|
+
except Exception as exc:
|
|
1797
|
+
logger.exception("Guidance generation failed")
|
|
1798
|
+
raise ToolError({"error": str(exc), "status": "error"}) from exc
|
|
1799
|
+
|
|
1800
|
+
return {"guidance": guidance}
|