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,210 @@
|
|
|
1
|
+
"""LLM backend abstraction for RAG and Cypher generation.
|
|
2
|
+
|
|
3
|
+
Provides a unified interface to call any OpenAI-compatible chat-completion API.
|
|
4
|
+
The provider is auto-detected from environment variables in this priority:
|
|
5
|
+
|
|
6
|
+
1. ``LLM_API_KEY`` / ``LLM_BASE_URL`` / ``LLM_MODEL`` (generic, highest)
|
|
7
|
+
2. ``OPENAI_API_KEY`` / ``OPENAI_BASE_URL`` / ``OPENAI_MODEL``
|
|
8
|
+
3. ``MOONSHOT_API_KEY`` / ``MOONSHOT_MODEL`` (legacy default)
|
|
9
|
+
|
|
10
|
+
When installed as an MCP server in Claude Code, configure the environment
|
|
11
|
+
variables in ``settings.json`` → ``mcpServers`` → ``env``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from loguru import logger
|
|
21
|
+
|
|
22
|
+
# Provider detection order: each tuple is (key_env, base_url_env, model_env, default_base_url, default_model)
|
|
23
|
+
_PROVIDER_ENVS: list[tuple[str, str, str, str, str]] = [
|
|
24
|
+
# Generic — user explicitly chose an LLM
|
|
25
|
+
("LLM_API_KEY", "LLM_BASE_URL", "LLM_MODEL", "https://api.openai.com/v1", "gpt-4o"),
|
|
26
|
+
# OpenAI / compatible (DeepSeek, Together, etc.)
|
|
27
|
+
("OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_MODEL", "https://api.openai.com/v1", "gpt-4o"),
|
|
28
|
+
# Moonshot / Kimi (legacy default)
|
|
29
|
+
("MOONSHOT_API_KEY", "LLM_BASE_URL", "MOONSHOT_MODEL", "https://api.moonshot.cn/v1", "kimi-k2.5"),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ToolCall:
|
|
35
|
+
"""A single tool invocation returned by the LLM."""
|
|
36
|
+
|
|
37
|
+
id: str
|
|
38
|
+
function_name: str
|
|
39
|
+
arguments: str # JSON-encoded string
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ChatMessage:
|
|
44
|
+
"""Structured response from a chat completion that may contain tool calls."""
|
|
45
|
+
|
|
46
|
+
content: str | None
|
|
47
|
+
tool_calls: list[ToolCall] | None
|
|
48
|
+
finish_reason: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class LLMBackend:
|
|
53
|
+
"""Generic LLM backend that calls an OpenAI-compatible chat-completion API."""
|
|
54
|
+
|
|
55
|
+
api_key: str = ""
|
|
56
|
+
model: str = "gpt-4o"
|
|
57
|
+
base_url: str = "https://api.openai.com/v1"
|
|
58
|
+
temperature: float = 1.0
|
|
59
|
+
max_tokens: int = 4096
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def available(self) -> bool:
|
|
63
|
+
"""Return *True* when an API key has been configured."""
|
|
64
|
+
return bool(self.api_key)
|
|
65
|
+
|
|
66
|
+
def chat(self, messages: list[dict[str, str]], **kwargs: Any) -> str:
|
|
67
|
+
"""Send a chat completion request and return the assistant's response text."""
|
|
68
|
+
try:
|
|
69
|
+
import httpx
|
|
70
|
+
except ImportError:
|
|
71
|
+
raise ImportError(
|
|
72
|
+
"httpx is required for LLM backend. "
|
|
73
|
+
"Install it with: pip install httpx"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
headers = {
|
|
77
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
}
|
|
80
|
+
payload: dict[str, Any] = {
|
|
81
|
+
"model": self.model,
|
|
82
|
+
"messages": messages,
|
|
83
|
+
"temperature": kwargs.get("temperature", self.temperature),
|
|
84
|
+
"max_tokens": kwargs.get("max_tokens", self.max_tokens),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
resp = httpx.post(
|
|
88
|
+
f"{self.base_url}/chat/completions",
|
|
89
|
+
json=payload,
|
|
90
|
+
headers=headers,
|
|
91
|
+
timeout=60.0,
|
|
92
|
+
)
|
|
93
|
+
resp.raise_for_status()
|
|
94
|
+
data = resp.json()
|
|
95
|
+
message = data["choices"][0]["message"]
|
|
96
|
+
return message.get("content") or message.get("reasoning_content", "")
|
|
97
|
+
|
|
98
|
+
def chat_with_tools(
|
|
99
|
+
self,
|
|
100
|
+
messages: list[dict[str, Any]],
|
|
101
|
+
tools: list[dict[str, Any]] | None = None,
|
|
102
|
+
**kwargs: Any,
|
|
103
|
+
) -> ChatMessage:
|
|
104
|
+
"""Send a chat completion with optional tool definitions.
|
|
105
|
+
|
|
106
|
+
Returns a :class:`ChatMessage` that may contain ``tool_calls`` when the
|
|
107
|
+
LLM decides to invoke one or more tools. If *tools* is ``None`` or
|
|
108
|
+
empty, behaves like :meth:`chat` but returns a structured message.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
import httpx
|
|
112
|
+
except ImportError:
|
|
113
|
+
raise ImportError(
|
|
114
|
+
"httpx is required for LLM backend. "
|
|
115
|
+
"Install it with: pip install httpx"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
headers = {
|
|
119
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
}
|
|
122
|
+
payload: dict[str, Any] = {
|
|
123
|
+
"model": self.model,
|
|
124
|
+
"messages": messages,
|
|
125
|
+
"temperature": kwargs.get("temperature", self.temperature),
|
|
126
|
+
"max_tokens": kwargs.get("max_tokens", self.max_tokens),
|
|
127
|
+
}
|
|
128
|
+
if tools:
|
|
129
|
+
payload["tools"] = tools
|
|
130
|
+
payload["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
131
|
+
|
|
132
|
+
resp = httpx.post(
|
|
133
|
+
f"{self.base_url}/chat/completions",
|
|
134
|
+
json=payload,
|
|
135
|
+
headers=headers,
|
|
136
|
+
timeout=kwargs.get("timeout", 120.0),
|
|
137
|
+
)
|
|
138
|
+
resp.raise_for_status()
|
|
139
|
+
data = resp.json()
|
|
140
|
+
|
|
141
|
+
choice = data["choices"][0]
|
|
142
|
+
message = choice["message"]
|
|
143
|
+
finish_reason = choice.get("finish_reason", "stop")
|
|
144
|
+
|
|
145
|
+
parsed_calls: list[ToolCall] | None = None
|
|
146
|
+
raw_calls = message.get("tool_calls")
|
|
147
|
+
if raw_calls:
|
|
148
|
+
parsed_calls = [
|
|
149
|
+
ToolCall(
|
|
150
|
+
id=tc["id"],
|
|
151
|
+
function_name=tc["function"]["name"],
|
|
152
|
+
arguments=tc["function"]["arguments"],
|
|
153
|
+
)
|
|
154
|
+
for tc in raw_calls
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
return ChatMessage(
|
|
158
|
+
content=message.get("content"),
|
|
159
|
+
tool_calls=parsed_calls,
|
|
160
|
+
finish_reason=finish_reason,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def create_llm_backend(**kwargs: Any) -> LLMBackend:
|
|
165
|
+
"""Create an LLM backend by auto-detecting available provider env vars.
|
|
166
|
+
|
|
167
|
+
Detection priority (first match wins):
|
|
168
|
+
1. ``LLM_API_KEY`` — generic override
|
|
169
|
+
2. ``OPENAI_API_KEY`` — OpenAI or any compatible endpoint
|
|
170
|
+
3. ``MOONSHOT_API_KEY`` — Moonshot / Kimi (legacy)
|
|
171
|
+
|
|
172
|
+
Any of these can be overridden by passing explicit keyword arguments
|
|
173
|
+
(``api_key``, ``base_url``, ``model``).
|
|
174
|
+
"""
|
|
175
|
+
explicit_key = kwargs.pop("api_key", None)
|
|
176
|
+
explicit_url = kwargs.pop("base_url", None)
|
|
177
|
+
explicit_model = kwargs.pop("model", None)
|
|
178
|
+
|
|
179
|
+
# Walk providers until we find one with a key
|
|
180
|
+
detected_key = ""
|
|
181
|
+
detected_url = ""
|
|
182
|
+
detected_model = ""
|
|
183
|
+
detected_provider = ""
|
|
184
|
+
|
|
185
|
+
for key_env, url_env, model_env, default_url, default_model in _PROVIDER_ENVS:
|
|
186
|
+
env_key = os.environ.get(key_env, "")
|
|
187
|
+
if env_key:
|
|
188
|
+
detected_key = env_key
|
|
189
|
+
detected_url = os.environ.get(url_env, default_url)
|
|
190
|
+
detected_model = os.environ.get(model_env, default_model)
|
|
191
|
+
detected_provider = key_env
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
api_key = explicit_key or detected_key
|
|
195
|
+
base_url = explicit_url or detected_url or "https://api.openai.com/v1"
|
|
196
|
+
model = explicit_model or detected_model or "gpt-4o"
|
|
197
|
+
|
|
198
|
+
if api_key:
|
|
199
|
+
logger.info(
|
|
200
|
+
f"LLM backend: model={model}, base_url={base_url} "
|
|
201
|
+
f"(detected via {detected_provider or 'explicit kwargs'})"
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
logger.warning(
|
|
205
|
+
"No LLM API key found in environment. "
|
|
206
|
+
"Set one of: LLM_API_KEY, OPENAI_API_KEY, or MOONSHOT_API_KEY. "
|
|
207
|
+
"Tools that require LLM (query_code_graph, wiki generation) will be unavailable."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return LLMBackend(api_key=api_key, base_url=base_url, model=model, **kwargs)
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Markdown output generator for RAG responses.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for generating well-formatted markdown
|
|
4
|
+
documentation from RAG analysis results.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
>>> from code_graph_builder.rag.markdown_generator import MarkdownGenerator
|
|
8
|
+
>>> generator = MarkdownGenerator()
|
|
9
|
+
>>> markdown = generator.generate_analysis_doc(
|
|
10
|
+
... title="Authentication System",
|
|
11
|
+
... query="Explain authentication",
|
|
12
|
+
... response="The auth system...",
|
|
13
|
+
... sources=[{"name": "auth.py", "path": "src/auth.py"}]
|
|
14
|
+
... )
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from datetime import UTC, datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
from loguru import logger
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from .prompt_templates import CodeContext
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class SourceReference:
|
|
32
|
+
"""Reference to a source code entity.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
name: Entity name
|
|
36
|
+
qualified_name: Fully qualified name
|
|
37
|
+
file_path: Source file path
|
|
38
|
+
line_start: Start line number
|
|
39
|
+
line_end: End line number
|
|
40
|
+
entity_type: Type of entity (Function, Class, etc.)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
qualified_name: str
|
|
45
|
+
file_path: str
|
|
46
|
+
line_start: int | None = None
|
|
47
|
+
line_end: int | None = None
|
|
48
|
+
entity_type: str | None = None
|
|
49
|
+
|
|
50
|
+
def format_link(self) -> str:
|
|
51
|
+
"""Format as markdown link."""
|
|
52
|
+
location = self.file_path
|
|
53
|
+
if self.line_start:
|
|
54
|
+
location += f":{self.line_start}"
|
|
55
|
+
if self.line_end and self.line_end != self.line_start:
|
|
56
|
+
location += f"-{self.line_end}"
|
|
57
|
+
return f"[{self.qualified_name}]({location})"
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> dict[str, Any]:
|
|
60
|
+
"""Convert to dictionary."""
|
|
61
|
+
return {
|
|
62
|
+
"name": self.name,
|
|
63
|
+
"qualified_name": self.qualified_name,
|
|
64
|
+
"file_path": self.file_path,
|
|
65
|
+
"line_start": self.line_start,
|
|
66
|
+
"line_end": self.line_end,
|
|
67
|
+
"entity_type": self.entity_type,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class AnalysisResult:
|
|
73
|
+
"""Result of a RAG analysis.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
query: Original user query
|
|
77
|
+
response: Generated response
|
|
78
|
+
sources: List of source references
|
|
79
|
+
metadata: Additional metadata
|
|
80
|
+
timestamp: Analysis timestamp
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
query: str
|
|
84
|
+
response: str
|
|
85
|
+
sources: list[SourceReference] = field(default_factory=list)
|
|
86
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
87
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict[str, Any]:
|
|
90
|
+
"""Convert to dictionary."""
|
|
91
|
+
return {
|
|
92
|
+
"query": self.query,
|
|
93
|
+
"response": self.response,
|
|
94
|
+
"sources": [s.to_dict() for s in self.sources],
|
|
95
|
+
"metadata": self.metadata,
|
|
96
|
+
"timestamp": self.timestamp.isoformat(),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MarkdownGenerator:
|
|
101
|
+
"""Generator for markdown documentation.
|
|
102
|
+
|
|
103
|
+
Creates well-formatted markdown documents from RAG analysis results.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
include_toc: Whether to include table of contents
|
|
107
|
+
include_timestamp: Whether to include generation timestamp
|
|
108
|
+
include_metadata: Whether to include metadata section
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
>>> generator = MarkdownGenerator(include_toc=True)
|
|
112
|
+
>>> doc = generator.generate_analysis_doc(
|
|
113
|
+
... title="Code Analysis",
|
|
114
|
+
... result=analysis_result
|
|
115
|
+
... )
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
include_toc: bool = True,
|
|
121
|
+
include_timestamp: bool = True,
|
|
122
|
+
include_metadata: bool = True,
|
|
123
|
+
):
|
|
124
|
+
self.include_toc = include_toc
|
|
125
|
+
self.include_timestamp = include_timestamp
|
|
126
|
+
self.include_metadata = include_metadata
|
|
127
|
+
|
|
128
|
+
def generate_analysis_doc(
|
|
129
|
+
self,
|
|
130
|
+
title: str,
|
|
131
|
+
result: AnalysisResult,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Generate markdown document from analysis result.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
title: Document title
|
|
137
|
+
result: Analysis result
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Markdown document as string
|
|
141
|
+
"""
|
|
142
|
+
lines = []
|
|
143
|
+
|
|
144
|
+
# Title
|
|
145
|
+
lines.append(f"# {title}")
|
|
146
|
+
lines.append("")
|
|
147
|
+
|
|
148
|
+
# Timestamp
|
|
149
|
+
if self.include_timestamp:
|
|
150
|
+
lines.append(f"*Generated: {result.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC')}*")
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
# Table of Contents
|
|
154
|
+
if self.include_toc:
|
|
155
|
+
lines.append("## Table of Contents")
|
|
156
|
+
lines.append("")
|
|
157
|
+
lines.append("- [Query](#query)")
|
|
158
|
+
lines.append("- [Analysis](#analysis)")
|
|
159
|
+
if result.sources:
|
|
160
|
+
lines.append("- [Sources](#sources)")
|
|
161
|
+
if self.include_metadata and result.metadata:
|
|
162
|
+
lines.append("- [Metadata](#metadata)")
|
|
163
|
+
lines.append("")
|
|
164
|
+
|
|
165
|
+
# Query section
|
|
166
|
+
lines.append("## Query")
|
|
167
|
+
lines.append("")
|
|
168
|
+
lines.append(f"> {result.query}")
|
|
169
|
+
lines.append("")
|
|
170
|
+
|
|
171
|
+
# Analysis section
|
|
172
|
+
lines.append("## Analysis")
|
|
173
|
+
lines.append("")
|
|
174
|
+
lines.append(result.response)
|
|
175
|
+
lines.append("")
|
|
176
|
+
|
|
177
|
+
# Sources section
|
|
178
|
+
if result.sources:
|
|
179
|
+
lines.append("## Sources")
|
|
180
|
+
lines.append("")
|
|
181
|
+
for i, source in enumerate(result.sources, 1):
|
|
182
|
+
lines.append(f"{i}. {source.format_link()}")
|
|
183
|
+
if source.entity_type:
|
|
184
|
+
lines.append(f" - Type: {source.entity_type}")
|
|
185
|
+
lines.append("")
|
|
186
|
+
|
|
187
|
+
# Metadata section
|
|
188
|
+
if self.include_metadata and result.metadata:
|
|
189
|
+
lines.append("## Metadata")
|
|
190
|
+
lines.append("")
|
|
191
|
+
for key, value in result.metadata.items():
|
|
192
|
+
lines.append(f"- **{key}**: {value}")
|
|
193
|
+
lines.append("")
|
|
194
|
+
|
|
195
|
+
return "\n".join(lines)
|
|
196
|
+
|
|
197
|
+
def generate_code_documentation(
|
|
198
|
+
self,
|
|
199
|
+
context: CodeContext,
|
|
200
|
+
analysis: str,
|
|
201
|
+
) -> str:
|
|
202
|
+
"""Generate documentation for a code entity.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
context: Code context
|
|
206
|
+
analysis: Analysis text
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Markdown documentation
|
|
210
|
+
"""
|
|
211
|
+
lines = []
|
|
212
|
+
|
|
213
|
+
# Title
|
|
214
|
+
title = context.qualified_name or context.entity_type or "Code Documentation"
|
|
215
|
+
lines.append(f"# {title}")
|
|
216
|
+
lines.append("")
|
|
217
|
+
|
|
218
|
+
# Entity info
|
|
219
|
+
if context.entity_type:
|
|
220
|
+
lines.append(f"**Type:** {context.entity_type}")
|
|
221
|
+
if context.file_path:
|
|
222
|
+
lines.append(f"**File:** `{context.file_path}`")
|
|
223
|
+
lines.append("")
|
|
224
|
+
|
|
225
|
+
# Documentation
|
|
226
|
+
lines.append(analysis)
|
|
227
|
+
lines.append("")
|
|
228
|
+
|
|
229
|
+
# Source code
|
|
230
|
+
lines.append("## Source Code")
|
|
231
|
+
lines.append("")
|
|
232
|
+
lines.append("```python")
|
|
233
|
+
lines.append(context.source_code)
|
|
234
|
+
lines.append("```")
|
|
235
|
+
lines.append("")
|
|
236
|
+
|
|
237
|
+
# Relationships
|
|
238
|
+
if context.callers:
|
|
239
|
+
lines.append("## Called By")
|
|
240
|
+
lines.append("")
|
|
241
|
+
for caller in context.callers:
|
|
242
|
+
lines.append(f"- `{caller}`")
|
|
243
|
+
lines.append("")
|
|
244
|
+
|
|
245
|
+
if context.callees:
|
|
246
|
+
lines.append("## Calls")
|
|
247
|
+
lines.append("")
|
|
248
|
+
for callee in context.callees:
|
|
249
|
+
lines.append(f"- `{callee}`")
|
|
250
|
+
lines.append("")
|
|
251
|
+
|
|
252
|
+
return "\n".join(lines)
|
|
253
|
+
|
|
254
|
+
def generate_comparison_doc(
|
|
255
|
+
self,
|
|
256
|
+
title: str,
|
|
257
|
+
query: str,
|
|
258
|
+
contexts: list[CodeContext],
|
|
259
|
+
analysis: str,
|
|
260
|
+
) -> str:
|
|
261
|
+
"""Generate comparison document for multiple code entities.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
title: Document title
|
|
265
|
+
query: Original query
|
|
266
|
+
contexts: List of code contexts
|
|
267
|
+
analysis: Comparative analysis
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Markdown document
|
|
271
|
+
"""
|
|
272
|
+
lines = []
|
|
273
|
+
|
|
274
|
+
lines.append(f"# {title}")
|
|
275
|
+
lines.append("")
|
|
276
|
+
lines.append(f"**Query:** {query}")
|
|
277
|
+
lines.append("")
|
|
278
|
+
|
|
279
|
+
lines.append("## Comparison Analysis")
|
|
280
|
+
lines.append("")
|
|
281
|
+
lines.append(analysis)
|
|
282
|
+
lines.append("")
|
|
283
|
+
|
|
284
|
+
lines.append("## Compared Entities")
|
|
285
|
+
lines.append("")
|
|
286
|
+
for i, ctx in enumerate(contexts, 1):
|
|
287
|
+
name = ctx.qualified_name or f"Entity {i}"
|
|
288
|
+
lines.append(f"### {i}. {name}")
|
|
289
|
+
if ctx.file_path:
|
|
290
|
+
lines.append(f"**File:** `{ctx.file_path}`")
|
|
291
|
+
lines.append("")
|
|
292
|
+
lines.append("```python")
|
|
293
|
+
lines.append(ctx.source_code[:500] + "..." if len(ctx.source_code) > 500 else ctx.source_code)
|
|
294
|
+
lines.append("```")
|
|
295
|
+
lines.append("")
|
|
296
|
+
|
|
297
|
+
return "\n".join(lines)
|
|
298
|
+
|
|
299
|
+
def save_document(
|
|
300
|
+
self,
|
|
301
|
+
content: str,
|
|
302
|
+
output_path: str | Path,
|
|
303
|
+
) -> Path:
|
|
304
|
+
"""Save markdown document to file.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
content: Markdown content
|
|
308
|
+
output_path: Output file path
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Path to saved file
|
|
312
|
+
"""
|
|
313
|
+
output_path = Path(output_path)
|
|
314
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
315
|
+
|
|
316
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
317
|
+
f.write(content)
|
|
318
|
+
|
|
319
|
+
logger.info(f"Saved markdown document to {output_path}")
|
|
320
|
+
return output_path
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def create_source_reference_from_context(
|
|
324
|
+
context: CodeContext,
|
|
325
|
+
) -> SourceReference:
|
|
326
|
+
"""Create a SourceReference from CodeContext.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
context: Code context
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
SourceReference instance
|
|
333
|
+
"""
|
|
334
|
+
return SourceReference(
|
|
335
|
+
name=context.qualified_name.split(".")[-1] if context.qualified_name else "unknown",
|
|
336
|
+
qualified_name=context.qualified_name or "unknown",
|
|
337
|
+
file_path=context.file_path or "",
|
|
338
|
+
entity_type=context.entity_type,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def format_code_block(code: str, language: str = "python") -> str:
|
|
343
|
+
"""Format code as markdown code block.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
code: Source code
|
|
347
|
+
language: Language identifier
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Formatted code block
|
|
351
|
+
"""
|
|
352
|
+
return f"```{language}\n{code}\n```"
|