codewiki 0.1.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 (194) hide show
  1. backend/__init__.py +2 -0
  2. backend/app/__init__.py +2 -0
  3. backend/app/api/__init__.py +2 -0
  4. backend/app/api/ask.py +37 -0
  5. backend/app/api/dependencies.py +63 -0
  6. backend/app/api/files.py +101 -0
  7. backend/app/api/graph.py +183 -0
  8. backend/app/api/repos.py +74 -0
  9. backend/app/api/runs.py +148 -0
  10. backend/app/api/settings.py +50 -0
  11. backend/app/api/wiki.py +224 -0
  12. backend/app/cli.py +538 -0
  13. backend/app/config.py +49 -0
  14. backend/app/database.py +25 -0
  15. backend/app/db/__init__.py +24 -0
  16. backend/app/db/base.py +124 -0
  17. backend/app/db/mappers.py +158 -0
  18. backend/app/db/records.py +21 -0
  19. backend/app/db/repositories/__init__.py +21 -0
  20. backend/app/db/repositories/analysis_runs.py +56 -0
  21. backend/app/db/repositories/code_chunks.py +145 -0
  22. backend/app/db/repositories/code_graph.py +101 -0
  23. backend/app/db/repositories/communities.py +41 -0
  24. backend/app/db/repositories/embeddings.py +210 -0
  25. backend/app/db/repositories/graphrag.py +11 -0
  26. backend/app/db/repositories/llm_runs.py +102 -0
  27. backend/app/db/repositories/repos.py +83 -0
  28. backend/app/db/repositories/wiki.py +184 -0
  29. backend/app/db/schema.py +12 -0
  30. backend/app/db/store.py +30 -0
  31. backend/app/db/utils.py +14 -0
  32. backend/app/main.py +35 -0
  33. backend/app/models/__init__.py +20 -0
  34. backend/app/models/base.py +48 -0
  35. backend/app/models/graph.py +117 -0
  36. backend/app/models/rag.py +109 -0
  37. backend/app/models/repo.py +115 -0
  38. backend/app/models/wiki.py +72 -0
  39. backend/app/prompts/catalog.md +112 -0
  40. backend/app/prompts/community_summary.md +18 -0
  41. backend/app/prompts/page.md +104 -0
  42. backend/app/prompts/qa.md +8 -0
  43. backend/app/prompts/translation.md +26 -0
  44. backend/app/schemas/__init__.py +2 -0
  45. backend/app/schemas/ask.py +26 -0
  46. backend/app/schemas/graph.py +43 -0
  47. backend/app/schemas/wiki.py +25 -0
  48. backend/app/services/__init__.py +2 -0
  49. backend/app/services/analysis_pipeline.py +91 -0
  50. backend/app/services/analyzer.py +164 -0
  51. backend/app/services/ast_cache.py +86 -0
  52. backend/app/services/ast_parser.py +65 -0
  53. backend/app/services/ast_parsers/__init__.py +32 -0
  54. backend/app/services/ast_parsers/augmenters/__init__.py +1 -0
  55. backend/app/services/ast_parsers/augmenters/c.py +12 -0
  56. backend/app/services/ast_parsers/augmenters/capture_only.py +14 -0
  57. backend/app/services/ast_parsers/augmenters/cpp.py +12 -0
  58. backend/app/services/ast_parsers/augmenters/csharp.py +12 -0
  59. backend/app/services/ast_parsers/augmenters/ecma.py +43 -0
  60. backend/app/services/ast_parsers/augmenters/go.py +390 -0
  61. backend/app/services/ast_parsers/augmenters/java.py +398 -0
  62. backend/app/services/ast_parsers/augmenters/python.py +238 -0
  63. backend/app/services/ast_parsers/augmenters/rust.py +12 -0
  64. backend/app/services/ast_parsers/base.py +33 -0
  65. backend/app/services/ast_parsers/c.py +28 -0
  66. backend/app/services/ast_parsers/capture_engine/__init__.py +10 -0
  67. backend/app/services/ast_parsers/capture_engine/captures.py +144 -0
  68. backend/app/services/ast_parsers/capture_engine/models.py +36 -0
  69. backend/app/services/ast_parsers/capture_engine/normalization.py +24 -0
  70. backend/app/services/ast_parsers/capture_engine/parser.py +117 -0
  71. backend/app/services/ast_parsers/capture_engine/symbols.py +50 -0
  72. backend/app/services/ast_parsers/capture_engine/topology.py +26 -0
  73. backend/app/services/ast_parsers/capture_specs/__init__.py +1 -0
  74. backend/app/services/ast_parsers/capture_specs/c.py +16 -0
  75. backend/app/services/ast_parsers/capture_specs/cpp.py +37 -0
  76. backend/app/services/ast_parsers/capture_specs/csharp.py +34 -0
  77. backend/app/services/ast_parsers/capture_specs/ecma.py +67 -0
  78. backend/app/services/ast_parsers/capture_specs/go.py +43 -0
  79. backend/app/services/ast_parsers/capture_specs/java.py +35 -0
  80. backend/app/services/ast_parsers/capture_specs/python.py +24 -0
  81. backend/app/services/ast_parsers/capture_specs/rust.py +34 -0
  82. backend/app/services/ast_parsers/common.py +15 -0
  83. backend/app/services/ast_parsers/cpp.py +28 -0
  84. backend/app/services/ast_parsers/csharp.py +28 -0
  85. backend/app/services/ast_parsers/ecma/__init__.py +6 -0
  86. backend/app/services/ast_parsers/ecma/declarations.py +286 -0
  87. backend/app/services/ast_parsers/ecma/endpoints.py +83 -0
  88. backend/app/services/ast_parsers/ecma/imports.py +27 -0
  89. backend/app/services/ast_parsers/ecma/parser.py +52 -0
  90. backend/app/services/ast_parsers/ecma/schemas.py +74 -0
  91. backend/app/services/ast_parsers/go.py +28 -0
  92. backend/app/services/ast_parsers/java.py +28 -0
  93. backend/app/services/ast_parsers/python.py +28 -0
  94. backend/app/services/ast_parsers/registry.py +100 -0
  95. backend/app/services/ast_parsers/rust.py +28 -0
  96. backend/app/services/ast_parsers/tree.py +40 -0
  97. backend/app/services/chunk_builder.py +84 -0
  98. backend/app/services/community_detector.py +172 -0
  99. backend/app/services/community_namer.py +129 -0
  100. backend/app/services/community_naming/__init__.py +49 -0
  101. backend/app/services/community_naming/batching.py +6 -0
  102. backend/app/services/community_naming/constants.py +6 -0
  103. backend/app/services/community_naming/fallback.py +66 -0
  104. backend/app/services/community_naming/models.py +12 -0
  105. backend/app/services/community_naming/payloads.py +116 -0
  106. backend/app/services/community_naming/response.py +135 -0
  107. backend/app/services/community_records.py +220 -0
  108. backend/app/services/embedding_index.py +115 -0
  109. backend/app/services/graph/__init__.py +4 -0
  110. backend/app/services/graph/builder.py +459 -0
  111. backend/app/services/graph/call_resolver.py +117 -0
  112. backend/app/services/graph/confidence.py +49 -0
  113. backend/app/services/graph/config_detector.py +140 -0
  114. backend/app/services/graph/ids.py +22 -0
  115. backend/app/services/graph/import_resolver.py +130 -0
  116. backend/app/services/graph/models.py +37 -0
  117. backend/app/services/graph/node_factory.py +169 -0
  118. backend/app/services/graph/nodes.py +23 -0
  119. backend/app/services/graph_provenance.py +127 -0
  120. backend/app/services/graphrag/__init__.py +5 -0
  121. backend/app/services/graphrag/chunking.py +3 -0
  122. backend/app/services/graphrag/constants.py +19 -0
  123. backend/app/services/graphrag/context.py +175 -0
  124. backend/app/services/graphrag/embedding.py +3 -0
  125. backend/app/services/graphrag/expansion.py +74 -0
  126. backend/app/services/graphrag/indexer.py +42 -0
  127. backend/app/services/graphrag/models.py +41 -0
  128. backend/app/services/graphrag/ranking.py +197 -0
  129. backend/app/services/graphrag/retriever.py +189 -0
  130. backend/app/services/graphrag/search.py +103 -0
  131. backend/app/services/graphrag/utils.py +72 -0
  132. backend/app/services/incremental/__init__.py +19 -0
  133. backend/app/services/incremental/models.py +63 -0
  134. backend/app/services/incremental/planning.py +84 -0
  135. backend/app/services/incremental/symbol_recovery.py +81 -0
  136. backend/app/services/incremental/updater.py +198 -0
  137. backend/app/services/incremental/wiki_regeneration.py +49 -0
  138. backend/app/services/language_detector.py +94 -0
  139. backend/app/services/llm_gateway.py +125 -0
  140. backend/app/services/llm_operations.py +41 -0
  141. backend/app/services/llm_run_recorder.py +248 -0
  142. backend/app/services/model_router.py +92 -0
  143. backend/app/services/prompts.py +5 -0
  144. backend/app/services/question_answerer.py +109 -0
  145. backend/app/services/repo_context.py +192 -0
  146. backend/app/services/repo_metadata.py +85 -0
  147. backend/app/services/repo_scanner/__init__.py +42 -0
  148. backend/app/services/repo_scanner/file_info.py +34 -0
  149. backend/app/services/repo_scanner/filesystem.py +61 -0
  150. backend/app/services/repo_scanner/git.py +165 -0
  151. backend/app/services/repo_scanner/git_ops.py +32 -0
  152. backend/app/services/repo_scanner/ignore.py +91 -0
  153. backend/app/services/repo_scanner/models.py +32 -0
  154. backend/app/services/repo_scanner/scanner.py +135 -0
  155. backend/app/services/wiki/__init__.py +4 -0
  156. backend/app/services/wiki/agent_tools.py +117 -0
  157. backend/app/services/wiki/catalog/__init__.py +298 -0
  158. backend/app/services/wiki/catalog/source_hints.py +81 -0
  159. backend/app/services/wiki/catalog_generator.py +261 -0
  160. backend/app/services/wiki/catalog_planner.py +96 -0
  161. backend/app/services/wiki/diagrams/__init__.py +271 -0
  162. backend/app/services/wiki/diagrams/components.py +162 -0
  163. backend/app/services/wiki/diagrams/data_flow.py +40 -0
  164. backend/app/services/wiki/diagrams/data_model.py +220 -0
  165. backend/app/services/wiki/diagrams/models.py +55 -0
  166. backend/app/services/wiki/diagrams/rendering.py +165 -0
  167. backend/app/services/wiki/diagrams/sequence.py +45 -0
  168. backend/app/services/wiki/diagrams/symbol_flow.py +174 -0
  169. backend/app/services/wiki/generator.py +139 -0
  170. backend/app/services/wiki/incremental_strategy.py +86 -0
  171. backend/app/services/wiki/language.py +30 -0
  172. backend/app/services/wiki/markdown.py +22 -0
  173. backend/app/services/wiki/mermaid_validation.py +68 -0
  174. backend/app/services/wiki/page_generator.py +239 -0
  175. backend/app/services/wiki/page_orchestrator.py +212 -0
  176. backend/app/services/wiki/page_payload.py +90 -0
  177. backend/app/services/wiki/page_payload_context.py +151 -0
  178. backend/app/services/wiki/page_payload_template.py +175 -0
  179. backend/app/services/wiki/page_validation.py +145 -0
  180. backend/app/services/wiki/prompts.py +80 -0
  181. backend/app/services/wiki/sources/__init__.py +41 -0
  182. backend/app/services/wiki/sources/citations.py +272 -0
  183. backend/app/services/wiki/sources/rendering.py +279 -0
  184. backend/app/services/wiki/sources/urls.py +42 -0
  185. backend/app/services/wiki/translation.py +269 -0
  186. backend/app/services/wiki/translation_orchestrator.py +264 -0
  187. backend/app/services/wiki/translation_support.py +175 -0
  188. backend/app/services/wiki/tree.py +91 -0
  189. backend/app/services/wiki/utils.py +17 -0
  190. codewiki-0.1.0.dist-info/METADATA +289 -0
  191. codewiki-0.1.0.dist-info/RECORD +194 -0
  192. codewiki-0.1.0.dist-info/WHEEL +5 -0
  193. codewiki-0.1.0.dist-info/entry_points.txt +2 -0
  194. codewiki-0.1.0.dist-info/top_level.txt +1 -0
backend/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """Backend package for Code Wiki Platform."""
2
+
@@ -0,0 +1,2 @@
1
+ """FastAPI application package."""
2
+
@@ -0,0 +1,2 @@
1
+ """HTTP API routers."""
2
+
backend/app/api/ask.py ADDED
@@ -0,0 +1,37 @@
1
+ from fastapi import APIRouter, HTTPException
2
+
3
+ from backend.app.config import get_settings
4
+ from backend.app.database import get_store
5
+ from backend.app.schemas.ask import AskRequest, AskResponse
6
+ from backend.app.services.graphrag import GraphRAGRetriever
7
+ from backend.app.services.llm_gateway import LLMGateway
8
+ from backend.app.services.llm_run_recorder import LLMCallError
9
+ from backend.app.services.question_answerer import QuestionAnswerer
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ @router.post("/{repo_id}/ask")
15
+ async def ask_repo(repo_id: str, payload: AskRequest) -> AskResponse:
16
+ settings = get_settings()
17
+ store = get_store()
18
+ answerer = QuestionAnswerer(
19
+ GraphRAGRetriever(store=store, settings=settings),
20
+ LLMGateway(settings),
21
+ store=store,
22
+ )
23
+ try:
24
+ return await answerer.answer(repo_id, payload)
25
+ except LLMCallError as exc:
26
+ raise HTTPException(
27
+ status_code=502,
28
+ detail={
29
+ "message": str(exc),
30
+ "task_type": exc.task_type,
31
+ "run_id": exc.run_id,
32
+ },
33
+ ) from exc
34
+ except ValueError as exc:
35
+ message = str(exc)
36
+ status_code = 404 if message.startswith("Repository not found") else 400
37
+ raise HTTPException(status_code=status_code, detail=message) from exc
@@ -0,0 +1,63 @@
1
+ from functools import cached_property, lru_cache
2
+ from typing import Annotated
3
+
4
+ from fastapi import Depends
5
+
6
+ from backend.app.config import Settings, get_settings
7
+ from backend.app.database import SQLiteStore, get_store
8
+ from backend.app.services.graphrag import GraphRAGRetriever
9
+ from backend.app.services.incremental import IncrementalUpdater
10
+ from backend.app.services.llm_gateway import LLMGateway
11
+ from backend.app.services.wiki import WikiGenerator
12
+
13
+
14
+ class ServiceContainer:
15
+ def __init__(self, *, settings: Settings, store: SQLiteStore) -> None:
16
+ self.settings = settings
17
+ self.store = store
18
+
19
+ @cached_property
20
+ def llm_gateway(self) -> LLMGateway:
21
+ return LLMGateway(self.settings)
22
+
23
+ @cached_property
24
+ def graph_retriever(self) -> GraphRAGRetriever:
25
+ return GraphRAGRetriever(store=self.store, settings=self.settings)
26
+
27
+ @cached_property
28
+ def wiki_generator(self) -> WikiGenerator:
29
+ return WikiGenerator(
30
+ self.graph_retriever,
31
+ self.llm_gateway,
32
+ store=self.store,
33
+ settings=self.settings,
34
+ )
35
+
36
+ @cached_property
37
+ def incremental_updater(self) -> IncrementalUpdater:
38
+ return IncrementalUpdater(
39
+ store=self.store,
40
+ graphrag=self.graph_retriever,
41
+ )
42
+
43
+
44
+ @lru_cache
45
+ def get_service_container() -> ServiceContainer:
46
+ return ServiceContainer(settings=get_settings(), store=get_store())
47
+
48
+
49
+ def get_store_dependency() -> SQLiteStore:
50
+ return get_service_container().store
51
+
52
+
53
+ def get_wiki_generator() -> WikiGenerator:
54
+ return get_service_container().wiki_generator
55
+
56
+
57
+ def get_incremental_updater() -> IncrementalUpdater:
58
+ return get_service_container().incremental_updater
59
+
60
+
61
+ StoreDep = Annotated[SQLiteStore, Depends(get_store_dependency)]
62
+ WikiGeneratorDep = Annotated[WikiGenerator, Depends(get_wiki_generator)]
63
+ IncrementalUpdaterDep = Annotated[IncrementalUpdater, Depends(get_incremental_updater)]
@@ -0,0 +1,101 @@
1
+ from typing import Any
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+
5
+ from backend.app.database import get_store
6
+ from backend.app.services.repo_scanner import RepoDescriptor, RepoScanner, ScannedFile
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ @router.get("/{repo_id}/files")
12
+ async def list_repo_files(repo_id: str) -> dict[str, object]:
13
+ store = get_store()
14
+ repo = store.get_repo(repo_id)
15
+ if repo is None:
16
+ raise HTTPException(status_code=404, detail=f"Repository not found: {repo_id}")
17
+
18
+ try:
19
+ scan = RepoScanner().scan(repo.path, name=repo.name, source_type=repo.source_type)
20
+ except (FileNotFoundError, NotADirectoryError, ValueError) as exc:
21
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
22
+
23
+ return {
24
+ "repo_id": repo.id,
25
+ "root": _tree_payload(repo, scan.files),
26
+ "files": [_file_payload(scanned_file) for scanned_file in scan.files],
27
+ "scanned_count": scan.scanned_count,
28
+ "ignored_count": scan.ignored_count,
29
+ "skipped_count": scan.skipped_count,
30
+ }
31
+
32
+
33
+ def _tree_payload(repo: RepoDescriptor, files: list[ScannedFile]) -> dict[str, Any]:
34
+ root = _directory_node(repo.name, "")
35
+ directory_by_path: dict[str, dict[str, Any]] = {"": root}
36
+
37
+ for scanned_file in files:
38
+ parent = root
39
+ parts = [part for part in scanned_file.path.split("/") if part]
40
+ current_path_parts: list[str] = []
41
+ for directory_name in parts[:-1]:
42
+ current_path_parts.append(directory_name)
43
+ directory_path = "/".join(current_path_parts)
44
+ directory = directory_by_path.get(directory_path)
45
+ if directory is None:
46
+ directory = _directory_node(directory_name, directory_path)
47
+ directory_by_path[directory_path] = directory
48
+ parent["children"].append(directory)
49
+ parent = directory
50
+
51
+ if parts:
52
+ parent["children"].append(
53
+ {
54
+ "type": "file",
55
+ "name": parts[-1],
56
+ "path": scanned_file.path,
57
+ "language": scanned_file.language,
58
+ "is_source": scanned_file.is_source,
59
+ "size_bytes": scanned_file.size_bytes,
60
+ "sha256": scanned_file.sha256,
61
+ "modified_at": scanned_file.modified_at,
62
+ }
63
+ )
64
+
65
+ _sort_tree(root)
66
+ return root
67
+
68
+
69
+ def _directory_node(name: str, path: str) -> dict[str, Any]:
70
+ return {
71
+ "type": "directory",
72
+ "name": name,
73
+ "path": path,
74
+ "children": [],
75
+ }
76
+
77
+
78
+ def _sort_tree(node: dict[str, Any]) -> None:
79
+ children = node.get("children")
80
+ if not isinstance(children, list):
81
+ return
82
+ children.sort(
83
+ key=lambda item: (
84
+ 0 if item.get("type") == "directory" else 1,
85
+ str(item.get("name", "")).lower(),
86
+ )
87
+ )
88
+ for child in children:
89
+ if child.get("type") == "directory":
90
+ _sort_tree(child)
91
+
92
+
93
+ def _file_payload(scanned_file: ScannedFile) -> dict[str, object]:
94
+ return {
95
+ "path": scanned_file.path,
96
+ "language": scanned_file.language,
97
+ "is_source": scanned_file.is_source,
98
+ "size_bytes": scanned_file.size_bytes,
99
+ "sha256": scanned_file.sha256,
100
+ "modified_at": scanned_file.modified_at,
101
+ }
@@ -0,0 +1,183 @@
1
+ from dataclasses import asdict
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+ from pydantic import BaseModel
5
+
6
+ from backend.app.config import get_settings
7
+ from backend.app.database import get_store
8
+ from backend.app.schemas.graph import CodeEdge, CodeNode, GraphCommunity, GraphResponse
9
+ from backend.app.services.community_namer import CommunityNamer
10
+ from backend.app.services.graph_provenance import edge_provenance, node_confidence, node_provenance
11
+ from backend.app.services.graphrag import GraphRAGRetriever
12
+ from backend.app.services.llm_gateway import LLMGateway
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ class BuildGraphRAGRequest(BaseModel):
18
+ include_embeddings: bool = False
19
+
20
+
21
+ class RetrieveRequest(BaseModel):
22
+ query: str
23
+ max_hops: int = 2
24
+ include_embeddings: bool = False
25
+
26
+
27
+ class NameCommunitiesRequest(BaseModel):
28
+ max_communities: int = 40
29
+
30
+
31
+ @router.get("/{repo_id}/graph")
32
+ async def get_graph(repo_id: str) -> GraphResponse:
33
+ store = get_store()
34
+ if store.get_repo(repo_id) is None:
35
+ raise HTTPException(status_code=404, detail=f"Repository not found: {repo_id}")
36
+ nodes, edges = store.get_graph(repo_id)
37
+ communities = store.list_graph_communities(repo_id)
38
+ return GraphResponse(
39
+ repo_id=repo_id,
40
+ nodes=[
41
+ CodeNode(
42
+ id=node.id,
43
+ type=node.type,
44
+ name=node.name,
45
+ file_path=node.file_path,
46
+ start_line=node.start_line,
47
+ end_line=node.end_line,
48
+ language=node.language,
49
+ symbol_id=node.symbol_id,
50
+ confidence=node_confidence(node.metadata),
51
+ provenance=node_provenance(node.metadata),
52
+ metadata=node.metadata,
53
+ )
54
+ for node in nodes
55
+ ],
56
+ edges=[
57
+ CodeEdge(
58
+ id=edge.id,
59
+ source=edge.source_id,
60
+ target=edge.target_id,
61
+ type=edge.type,
62
+ confidence=edge.confidence,
63
+ confidence_level=(
64
+ str(edge.metadata["confidence_level"])
65
+ if isinstance(edge.metadata.get("confidence_level"), str)
66
+ else None
67
+ ),
68
+ reason=(
69
+ str(edge.metadata["reason"])
70
+ if isinstance(edge.metadata.get("reason"), str)
71
+ else None
72
+ ),
73
+ is_inferred=edge.is_inferred,
74
+ provenance=edge_provenance(edge.metadata),
75
+ metadata=edge.metadata,
76
+ )
77
+ for edge in edges
78
+ ],
79
+ communities=[
80
+ GraphCommunity(
81
+ id=community.id,
82
+ name=community.name,
83
+ level=community.level,
84
+ node_ids=community.node_ids,
85
+ summary=community.summary or "",
86
+ )
87
+ for community in communities
88
+ ],
89
+ )
90
+
91
+
92
+ @router.get("/{repo_id}/graph/nodes/{node_id}")
93
+ async def get_node(repo_id: str, node_id: str) -> dict[str, str]:
94
+ nodes, edges = get_store().get_graph(repo_id)
95
+ node = next((item for item in nodes if item.id == node_id), None)
96
+ if node is None:
97
+ raise HTTPException(status_code=404, detail=f"Node not found: {node_id}")
98
+ adjacent_edges = [
99
+ edge for edge in edges if edge.source_id == node_id or edge.target_id == node_id
100
+ ]
101
+ return {
102
+ "repo_id": repo_id,
103
+ "node_id": node_id,
104
+ "type": node.type,
105
+ "name": node.name,
106
+ "file_path": node.file_path,
107
+ "adjacent_edge_count": str(len(adjacent_edges)),
108
+ }
109
+
110
+
111
+ @router.get("/{repo_id}/communities")
112
+ async def get_communities(repo_id: str) -> list[dict[str, str]]:
113
+ return [
114
+ {
115
+ "id": community.id,
116
+ "name": community.name,
117
+ "level": str(community.level),
118
+ "summary": community.summary or "",
119
+ }
120
+ for community in get_store().list_graph_communities(repo_id)
121
+ ]
122
+
123
+
124
+ @router.post("/{repo_id}/communities/name")
125
+ async def name_communities(
126
+ repo_id: str,
127
+ payload: NameCommunitiesRequest | None = None,
128
+ ) -> dict[str, object]:
129
+ store = get_store()
130
+ if store.get_repo(repo_id) is None:
131
+ raise HTTPException(status_code=404, detail=f"Repository not found: {repo_id}")
132
+ request = payload or NameCommunitiesRequest()
133
+ try:
134
+ result = await CommunityNamer(
135
+ LLMGateway(get_settings()),
136
+ store=store,
137
+ ).summarize_communities(repo_id, max_communities=request.max_communities)
138
+ except ValueError as exc:
139
+ message = str(exc)
140
+ status_code = 404 if message.startswith("Repository not found") else 400
141
+ raise HTTPException(status_code=status_code, detail=message) from exc
142
+ return asdict(result)
143
+
144
+
145
+ @router.post("/{repo_id}/graphrag/build")
146
+ async def build_graphrag(
147
+ repo_id: str,
148
+ payload: BuildGraphRAGRequest | None = None,
149
+ ) -> dict[str, object]:
150
+ request = payload or BuildGraphRAGRequest()
151
+ store = get_store()
152
+ settings = get_settings()
153
+ try:
154
+ result = await GraphRAGRetriever(store=store, settings=settings).build_index(
155
+ repo_id,
156
+ include_embeddings=request.include_embeddings,
157
+ )
158
+ except ValueError as exc:
159
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
160
+ return asdict(result)
161
+
162
+
163
+ @router.post("/{repo_id}/graphrag/retrieve")
164
+ async def retrieve_context(repo_id: str, payload: RetrieveRequest) -> dict[str, object]:
165
+ store = get_store()
166
+ settings = get_settings()
167
+ try:
168
+ trace = await GraphRAGRetriever(store=store, settings=settings).retrieve(
169
+ repo_id,
170
+ payload.query,
171
+ max_hops=payload.max_hops,
172
+ include_embeddings=payload.include_embeddings,
173
+ )
174
+ except ValueError as exc:
175
+ message = str(exc)
176
+ status_code = 404 if message.startswith("Repository not found") else 400
177
+ raise HTTPException(status_code=status_code, detail=message) from exc
178
+ return asdict(trace)
179
+
180
+
181
+ @router.get("/{repo_id}/graphrag/traces/{trace_id}")
182
+ async def get_retrieval_trace(repo_id: str, trace_id: str) -> dict[str, object]:
183
+ return {"repo_id": repo_id, "trace_id": trace_id, "status": "not_persisted_yet"}
@@ -0,0 +1,74 @@
1
+ from fastapi import APIRouter, HTTPException, Response
2
+ from pydantic import BaseModel
3
+
4
+ from backend.app.database import get_store
5
+ from backend.app.services.repo_scanner import RepoDescriptor, RepoScanResult, RepoScanner
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ class CreateRepoRequest(BaseModel):
11
+ path: str
12
+ name: str | None = None
13
+ source_type: str = "local"
14
+
15
+
16
+ class ScanRepoRequest(CreateRepoRequest):
17
+ pass
18
+
19
+
20
+ @router.post("")
21
+ async def create_repo(payload: CreateRepoRequest) -> RepoDescriptor:
22
+ scanner = RepoScanner()
23
+ try:
24
+ repo = scanner.describe(payload.path, name=payload.name, source_type=payload.source_type)
25
+ except (FileNotFoundError, NotADirectoryError, ValueError) as exc:
26
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
27
+ return get_store().upsert_repo(repo)
28
+
29
+
30
+ @router.post("/scan")
31
+ async def scan_repo(payload: ScanRepoRequest) -> RepoScanResult:
32
+ scanner = RepoScanner()
33
+ try:
34
+ return scanner.scan(payload.path, name=payload.name, source_type=payload.source_type)
35
+ except (FileNotFoundError, NotADirectoryError, ValueError) as exc:
36
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
37
+
38
+
39
+ @router.get("")
40
+ async def list_repos() -> list[dict[str, str]]:
41
+ return [
42
+ {
43
+ "id": repo.id,
44
+ "name": repo.name,
45
+ "path": repo.path,
46
+ "source_type": repo.source_type,
47
+ "git_url": repo.git_url or "",
48
+ "commit_hash": repo.commit_hash or "",
49
+ }
50
+ for repo in get_store().list_repos()
51
+ ]
52
+
53
+
54
+ @router.get("/{repo_id}")
55
+ async def get_repo(repo_id: str) -> dict[str, str]:
56
+ repo = get_store().get_repo(repo_id)
57
+ if repo is None:
58
+ raise HTTPException(status_code=404, detail=f"Repository not found: {repo_id}")
59
+ return {
60
+ "id": repo.id,
61
+ "name": repo.name,
62
+ "path": repo.path,
63
+ "source_type": repo.source_type,
64
+ "git_url": repo.git_url or "",
65
+ "commit_hash": repo.commit_hash or "",
66
+ }
67
+
68
+
69
+ @router.delete("/{repo_id}", status_code=204)
70
+ async def delete_repo(repo_id: str) -> Response:
71
+ deleted = get_store().delete_repo(repo_id)
72
+ if not deleted:
73
+ raise HTTPException(status_code=404, detail=f"Repository not found: {repo_id}")
74
+ return Response(status_code=204)
@@ -0,0 +1,148 @@
1
+ from dataclasses import asdict
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+ from pydantic import BaseModel
5
+
6
+ from backend.app.config import get_settings
7
+ from backend.app.database import get_store
8
+ from backend.app.services.analyzer import AnalysisService, _llm_configured
9
+ from backend.app.services.community_namer import CommunityNamer
10
+ from backend.app.services.community_naming import CommunityNamingResult
11
+ from backend.app.services.incremental import IncrementalUpdater
12
+ from backend.app.services.llm_gateway import LLMGateway
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ class AnalyzeRepoRequest(BaseModel):
18
+ name_communities: bool = True
19
+
20
+
21
+ class IncrementalUpdateRequest(BaseModel):
22
+ refresh_chunks: bool = True
23
+ name_communities: bool = True
24
+ regenerate_wiki: bool = True
25
+
26
+
27
+ @router.post("/{repo_id}/analyze")
28
+ async def analyze_repo(repo_id: str, payload: AnalyzeRepoRequest | None = None) -> dict[str, object]:
29
+ store = get_store()
30
+ if store.get_repo(repo_id) is None:
31
+ raise HTTPException(status_code=404, detail=f"Repository not found: {repo_id}")
32
+ request = payload or AnalyzeRepoRequest()
33
+ try:
34
+ analysis = await AnalysisService(store=store).analyze_with_community_summaries(
35
+ repo_id,
36
+ name_communities=request.name_communities,
37
+ )
38
+ result = analysis.analysis
39
+ naming_result = analysis.community_naming
40
+ except ValueError as exc:
41
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
42
+ response = {
43
+ "run_id": result.run_id,
44
+ "repo_id": result.repo_id,
45
+ "status": result.status,
46
+ "scanned_count": result.scanned_count,
47
+ "parsed_file_count": result.parsed_file_count,
48
+ "node_count": result.node_count,
49
+ "edge_count": result.edge_count,
50
+ "community_count": result.community_count,
51
+ "errors": result.errors,
52
+ }
53
+ if naming_result is not None:
54
+ response["community_naming"] = asdict(naming_result)
55
+ return response
56
+
57
+
58
+ @router.post("/{repo_id}/update")
59
+ async def update_repo(
60
+ repo_id: str,
61
+ payload: IncrementalUpdateRequest | None = None,
62
+ ) -> dict[str, object]:
63
+ store = get_store()
64
+ if store.get_repo(repo_id) is None:
65
+ raise HTTPException(status_code=404, detail=f"Repository not found: {repo_id}")
66
+ request = payload or IncrementalUpdateRequest()
67
+ try:
68
+ result, wiki_regeneration = await IncrementalUpdater(store=store).update_with_wiki_regeneration(
69
+ repo_id,
70
+ refresh_chunks=request.refresh_chunks,
71
+ regenerate_wiki=request.regenerate_wiki,
72
+ )
73
+ naming_result = await _name_communities(repo_id) if request.name_communities else None
74
+ except ValueError as exc:
75
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
76
+ response = {
77
+ "run_id": result.run_id,
78
+ "repo_id": result.repo_id,
79
+ "status": result.status,
80
+ "plan": result.plan.as_dict(),
81
+ "scanned_count": result.scanned_count,
82
+ "parsed_file_count": result.parsed_file_count,
83
+ "reused_file_count": result.reused_file_count,
84
+ "node_count": result.node_count,
85
+ "edge_count": result.edge_count,
86
+ "community_count": result.community_count,
87
+ "chunk_count": result.chunk_count,
88
+ "stale_pages": result.stale_pages,
89
+ "wiki_regeneration": wiki_regeneration,
90
+ "errors": result.errors,
91
+ }
92
+ if naming_result is not None:
93
+ response["community_naming"] = asdict(naming_result)
94
+ return response
95
+
96
+
97
+ async def _name_communities(repo_id: str) -> CommunityNamingResult:
98
+ settings = get_settings()
99
+ if not _llm_configured(settings):
100
+ return CommunityNamingResult(
101
+ repo_id=repo_id,
102
+ status="skipped",
103
+ renamed_count=0,
104
+ community_count=len(get_store().list_graph_communities(repo_id)),
105
+ errors=["LLM community naming skipped because no LLM endpoint or API key is configured."],
106
+ )
107
+ try:
108
+ return await CommunityNamer(LLMGateway(settings), store=get_store()).summarize_communities(repo_id)
109
+ except Exception as exc:
110
+ return CommunityNamingResult(
111
+ repo_id=repo_id,
112
+ status="failed",
113
+ renamed_count=0,
114
+ community_count=len(get_store().list_graph_communities(repo_id)),
115
+ errors=[str(exc)],
116
+ )
117
+
118
+
119
+ @router.get("/{repo_id}/runs")
120
+ async def list_runs(repo_id: str) -> list[dict[str, object]]:
121
+ return [
122
+ {
123
+ "id": run.id,
124
+ "repo_id": run.repo_id,
125
+ "status": run.status,
126
+ "started_at": run.started_at,
127
+ "finished_at": run.finished_at,
128
+ "error": run.error,
129
+ "stats": run.stats,
130
+ }
131
+ for run in get_store().list_analysis_runs(repo_id)
132
+ ]
133
+
134
+
135
+ @router.get("/{repo_id}/runs/{run_id}")
136
+ async def get_run(repo_id: str, run_id: str) -> dict[str, object]:
137
+ run = get_store().get_analysis_run(run_id)
138
+ if run is None or run.repo_id != repo_id:
139
+ raise HTTPException(status_code=404, detail=f"Run not found: {run_id}")
140
+ return {
141
+ "id": run.id,
142
+ "repo_id": run.repo_id,
143
+ "status": run.status,
144
+ "started_at": run.started_at,
145
+ "finished_at": run.finished_at,
146
+ "error": run.error,
147
+ "stats": run.stats,
148
+ }
@@ -0,0 +1,50 @@
1
+ from fastapi import APIRouter
2
+ from pydantic import BaseModel
3
+
4
+ from backend.app.config import get_settings
5
+ from backend.app.services.model_router import ModelRouter
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ class TestModelRequest(BaseModel):
11
+ model: str | None = None
12
+ task_type: str = "qa"
13
+
14
+
15
+ @router.get("/llm/models")
16
+ async def get_llm_models() -> dict[str, object]:
17
+ settings = get_settings()
18
+ model_router = ModelRouter(settings)
19
+ profiles = {
20
+ task_type: _profile_payload(model_router.profile_for(task_type))
21
+ for task_type in (
22
+ "catalog",
23
+ "community_summary",
24
+ "page",
25
+ "translation",
26
+ "qa",
27
+ "embedding",
28
+ )
29
+ }
30
+ return {
31
+ "mode": settings.llm.mode,
32
+ "default_profile": _profile_payload(model_router.default_profile()),
33
+ "profiles": profiles,
34
+ }
35
+
36
+
37
+ @router.post("/llm/test")
38
+ async def test_llm_model(payload: TestModelRequest) -> dict[str, str]:
39
+ return {"status": "not_implemented", "task_type": payload.task_type, "model": payload.model or ""}
40
+
41
+
42
+ def _profile_payload(profile) -> dict[str, object]:
43
+ return {
44
+ "model": profile.model,
45
+ "provider_type": profile.provider_type or "",
46
+ "endpoint": profile.endpoint or "",
47
+ "has_api_key": bool(profile.api_key),
48
+ "stream": profile.stream,
49
+ "max_tokens": profile.max_tokens,
50
+ }