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.
Files changed (93) hide show
  1. code_graph_builder/__init__.py +82 -0
  2. code_graph_builder/builder.py +366 -0
  3. code_graph_builder/cgb_cli.py +32 -0
  4. code_graph_builder/cli.py +564 -0
  5. code_graph_builder/commands_cli.py +1288 -0
  6. code_graph_builder/config.py +340 -0
  7. code_graph_builder/constants.py +708 -0
  8. code_graph_builder/embeddings/__init__.py +40 -0
  9. code_graph_builder/embeddings/qwen3_embedder.py +573 -0
  10. code_graph_builder/embeddings/vector_store.py +584 -0
  11. code_graph_builder/examples/__init__.py +0 -0
  12. code_graph_builder/examples/example_configuration.py +276 -0
  13. code_graph_builder/examples/example_kuzu_usage.py +109 -0
  14. code_graph_builder/examples/example_semantic_search_full.py +347 -0
  15. code_graph_builder/examples/generate_wiki.py +915 -0
  16. code_graph_builder/examples/graph_export_example.py +100 -0
  17. code_graph_builder/examples/rag_example.py +206 -0
  18. code_graph_builder/examples/test_cli_demo.py +129 -0
  19. code_graph_builder/examples/test_embedding_api.py +153 -0
  20. code_graph_builder/examples/test_kuzu_local.py +190 -0
  21. code_graph_builder/examples/test_rag_redis.py +390 -0
  22. code_graph_builder/graph_updater.py +605 -0
  23. code_graph_builder/guidance/__init__.py +1 -0
  24. code_graph_builder/guidance/agent.py +123 -0
  25. code_graph_builder/guidance/prompts.py +74 -0
  26. code_graph_builder/guidance/toolset.py +264 -0
  27. code_graph_builder/language_spec.py +536 -0
  28. code_graph_builder/mcp/__init__.py +21 -0
  29. code_graph_builder/mcp/api_doc_generator.py +764 -0
  30. code_graph_builder/mcp/file_editor.py +207 -0
  31. code_graph_builder/mcp/pipeline.py +777 -0
  32. code_graph_builder/mcp/server.py +161 -0
  33. code_graph_builder/mcp/tools.py +1800 -0
  34. code_graph_builder/models.py +115 -0
  35. code_graph_builder/parser_loader.py +344 -0
  36. code_graph_builder/parsers/__init__.py +7 -0
  37. code_graph_builder/parsers/call_processor.py +306 -0
  38. code_graph_builder/parsers/call_resolver.py +139 -0
  39. code_graph_builder/parsers/definition_processor.py +796 -0
  40. code_graph_builder/parsers/factory.py +119 -0
  41. code_graph_builder/parsers/import_processor.py +293 -0
  42. code_graph_builder/parsers/structure_processor.py +145 -0
  43. code_graph_builder/parsers/type_inference.py +143 -0
  44. code_graph_builder/parsers/utils.py +134 -0
  45. code_graph_builder/rag/__init__.py +68 -0
  46. code_graph_builder/rag/camel_agent.py +429 -0
  47. code_graph_builder/rag/client.py +298 -0
  48. code_graph_builder/rag/config.py +239 -0
  49. code_graph_builder/rag/cypher_generator.py +67 -0
  50. code_graph_builder/rag/llm_backend.py +210 -0
  51. code_graph_builder/rag/markdown_generator.py +352 -0
  52. code_graph_builder/rag/prompt_templates.py +440 -0
  53. code_graph_builder/rag/rag_engine.py +640 -0
  54. code_graph_builder/rag/review_report.md +172 -0
  55. code_graph_builder/rag/tests/__init__.py +3 -0
  56. code_graph_builder/rag/tests/test_camel_agent.py +313 -0
  57. code_graph_builder/rag/tests/test_client.py +221 -0
  58. code_graph_builder/rag/tests/test_config.py +177 -0
  59. code_graph_builder/rag/tests/test_markdown_generator.py +240 -0
  60. code_graph_builder/rag/tests/test_prompt_templates.py +160 -0
  61. code_graph_builder/services/__init__.py +39 -0
  62. code_graph_builder/services/graph_service.py +465 -0
  63. code_graph_builder/services/kuzu_service.py +665 -0
  64. code_graph_builder/services/memory_service.py +171 -0
  65. code_graph_builder/settings.py +75 -0
  66. code_graph_builder/tests/ACCEPTANCE_CRITERIA_PHASE2.md +401 -0
  67. code_graph_builder/tests/__init__.py +1 -0
  68. code_graph_builder/tests/run_acceptance_check.py +378 -0
  69. code_graph_builder/tests/test_api_find.py +231 -0
  70. code_graph_builder/tests/test_api_find_integration.py +226 -0
  71. code_graph_builder/tests/test_basic.py +78 -0
  72. code_graph_builder/tests/test_c_api_extraction.py +388 -0
  73. code_graph_builder/tests/test_call_resolution_scenarios.py +504 -0
  74. code_graph_builder/tests/test_embedder.py +411 -0
  75. code_graph_builder/tests/test_integration_semantic.py +434 -0
  76. code_graph_builder/tests/test_mcp_protocol.py +298 -0
  77. code_graph_builder/tests/test_mcp_user_flow.py +190 -0
  78. code_graph_builder/tests/test_rag.py +404 -0
  79. code_graph_builder/tests/test_settings.py +135 -0
  80. code_graph_builder/tests/test_step1_graph_build.py +264 -0
  81. code_graph_builder/tests/test_step2_api_docs.py +323 -0
  82. code_graph_builder/tests/test_step3_embedding.py +278 -0
  83. code_graph_builder/tests/test_vector_store.py +552 -0
  84. code_graph_builder/tools/__init__.py +40 -0
  85. code_graph_builder/tools/graph_query.py +495 -0
  86. code_graph_builder/tools/semantic_search.py +387 -0
  87. code_graph_builder/types.py +333 -0
  88. code_graph_builder/utils/__init__.py +0 -0
  89. code_graph_builder/utils/path_utils.py +30 -0
  90. code_graph_builder-0.2.0.dist-info/METADATA +321 -0
  91. code_graph_builder-0.2.0.dist-info/RECORD +93 -0
  92. code_graph_builder-0.2.0.dist-info/WHEEL +4 -0
  93. 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```"