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.
- backend/__init__.py +2 -0
- backend/app/__init__.py +2 -0
- backend/app/api/__init__.py +2 -0
- backend/app/api/ask.py +37 -0
- backend/app/api/dependencies.py +63 -0
- backend/app/api/files.py +101 -0
- backend/app/api/graph.py +183 -0
- backend/app/api/repos.py +74 -0
- backend/app/api/runs.py +148 -0
- backend/app/api/settings.py +50 -0
- backend/app/api/wiki.py +224 -0
- backend/app/cli.py +538 -0
- backend/app/config.py +49 -0
- backend/app/database.py +25 -0
- backend/app/db/__init__.py +24 -0
- backend/app/db/base.py +124 -0
- backend/app/db/mappers.py +158 -0
- backend/app/db/records.py +21 -0
- backend/app/db/repositories/__init__.py +21 -0
- backend/app/db/repositories/analysis_runs.py +56 -0
- backend/app/db/repositories/code_chunks.py +145 -0
- backend/app/db/repositories/code_graph.py +101 -0
- backend/app/db/repositories/communities.py +41 -0
- backend/app/db/repositories/embeddings.py +210 -0
- backend/app/db/repositories/graphrag.py +11 -0
- backend/app/db/repositories/llm_runs.py +102 -0
- backend/app/db/repositories/repos.py +83 -0
- backend/app/db/repositories/wiki.py +184 -0
- backend/app/db/schema.py +12 -0
- backend/app/db/store.py +30 -0
- backend/app/db/utils.py +14 -0
- backend/app/main.py +35 -0
- backend/app/models/__init__.py +20 -0
- backend/app/models/base.py +48 -0
- backend/app/models/graph.py +117 -0
- backend/app/models/rag.py +109 -0
- backend/app/models/repo.py +115 -0
- backend/app/models/wiki.py +72 -0
- backend/app/prompts/catalog.md +112 -0
- backend/app/prompts/community_summary.md +18 -0
- backend/app/prompts/page.md +104 -0
- backend/app/prompts/qa.md +8 -0
- backend/app/prompts/translation.md +26 -0
- backend/app/schemas/__init__.py +2 -0
- backend/app/schemas/ask.py +26 -0
- backend/app/schemas/graph.py +43 -0
- backend/app/schemas/wiki.py +25 -0
- backend/app/services/__init__.py +2 -0
- backend/app/services/analysis_pipeline.py +91 -0
- backend/app/services/analyzer.py +164 -0
- backend/app/services/ast_cache.py +86 -0
- backend/app/services/ast_parser.py +65 -0
- backend/app/services/ast_parsers/__init__.py +32 -0
- backend/app/services/ast_parsers/augmenters/__init__.py +1 -0
- backend/app/services/ast_parsers/augmenters/c.py +12 -0
- backend/app/services/ast_parsers/augmenters/capture_only.py +14 -0
- backend/app/services/ast_parsers/augmenters/cpp.py +12 -0
- backend/app/services/ast_parsers/augmenters/csharp.py +12 -0
- backend/app/services/ast_parsers/augmenters/ecma.py +43 -0
- backend/app/services/ast_parsers/augmenters/go.py +390 -0
- backend/app/services/ast_parsers/augmenters/java.py +398 -0
- backend/app/services/ast_parsers/augmenters/python.py +238 -0
- backend/app/services/ast_parsers/augmenters/rust.py +12 -0
- backend/app/services/ast_parsers/base.py +33 -0
- backend/app/services/ast_parsers/c.py +28 -0
- backend/app/services/ast_parsers/capture_engine/__init__.py +10 -0
- backend/app/services/ast_parsers/capture_engine/captures.py +144 -0
- backend/app/services/ast_parsers/capture_engine/models.py +36 -0
- backend/app/services/ast_parsers/capture_engine/normalization.py +24 -0
- backend/app/services/ast_parsers/capture_engine/parser.py +117 -0
- backend/app/services/ast_parsers/capture_engine/symbols.py +50 -0
- backend/app/services/ast_parsers/capture_engine/topology.py +26 -0
- backend/app/services/ast_parsers/capture_specs/__init__.py +1 -0
- backend/app/services/ast_parsers/capture_specs/c.py +16 -0
- backend/app/services/ast_parsers/capture_specs/cpp.py +37 -0
- backend/app/services/ast_parsers/capture_specs/csharp.py +34 -0
- backend/app/services/ast_parsers/capture_specs/ecma.py +67 -0
- backend/app/services/ast_parsers/capture_specs/go.py +43 -0
- backend/app/services/ast_parsers/capture_specs/java.py +35 -0
- backend/app/services/ast_parsers/capture_specs/python.py +24 -0
- backend/app/services/ast_parsers/capture_specs/rust.py +34 -0
- backend/app/services/ast_parsers/common.py +15 -0
- backend/app/services/ast_parsers/cpp.py +28 -0
- backend/app/services/ast_parsers/csharp.py +28 -0
- backend/app/services/ast_parsers/ecma/__init__.py +6 -0
- backend/app/services/ast_parsers/ecma/declarations.py +286 -0
- backend/app/services/ast_parsers/ecma/endpoints.py +83 -0
- backend/app/services/ast_parsers/ecma/imports.py +27 -0
- backend/app/services/ast_parsers/ecma/parser.py +52 -0
- backend/app/services/ast_parsers/ecma/schemas.py +74 -0
- backend/app/services/ast_parsers/go.py +28 -0
- backend/app/services/ast_parsers/java.py +28 -0
- backend/app/services/ast_parsers/python.py +28 -0
- backend/app/services/ast_parsers/registry.py +100 -0
- backend/app/services/ast_parsers/rust.py +28 -0
- backend/app/services/ast_parsers/tree.py +40 -0
- backend/app/services/chunk_builder.py +84 -0
- backend/app/services/community_detector.py +172 -0
- backend/app/services/community_namer.py +129 -0
- backend/app/services/community_naming/__init__.py +49 -0
- backend/app/services/community_naming/batching.py +6 -0
- backend/app/services/community_naming/constants.py +6 -0
- backend/app/services/community_naming/fallback.py +66 -0
- backend/app/services/community_naming/models.py +12 -0
- backend/app/services/community_naming/payloads.py +116 -0
- backend/app/services/community_naming/response.py +135 -0
- backend/app/services/community_records.py +220 -0
- backend/app/services/embedding_index.py +115 -0
- backend/app/services/graph/__init__.py +4 -0
- backend/app/services/graph/builder.py +459 -0
- backend/app/services/graph/call_resolver.py +117 -0
- backend/app/services/graph/confidence.py +49 -0
- backend/app/services/graph/config_detector.py +140 -0
- backend/app/services/graph/ids.py +22 -0
- backend/app/services/graph/import_resolver.py +130 -0
- backend/app/services/graph/models.py +37 -0
- backend/app/services/graph/node_factory.py +169 -0
- backend/app/services/graph/nodes.py +23 -0
- backend/app/services/graph_provenance.py +127 -0
- backend/app/services/graphrag/__init__.py +5 -0
- backend/app/services/graphrag/chunking.py +3 -0
- backend/app/services/graphrag/constants.py +19 -0
- backend/app/services/graphrag/context.py +175 -0
- backend/app/services/graphrag/embedding.py +3 -0
- backend/app/services/graphrag/expansion.py +74 -0
- backend/app/services/graphrag/indexer.py +42 -0
- backend/app/services/graphrag/models.py +41 -0
- backend/app/services/graphrag/ranking.py +197 -0
- backend/app/services/graphrag/retriever.py +189 -0
- backend/app/services/graphrag/search.py +103 -0
- backend/app/services/graphrag/utils.py +72 -0
- backend/app/services/incremental/__init__.py +19 -0
- backend/app/services/incremental/models.py +63 -0
- backend/app/services/incremental/planning.py +84 -0
- backend/app/services/incremental/symbol_recovery.py +81 -0
- backend/app/services/incremental/updater.py +198 -0
- backend/app/services/incremental/wiki_regeneration.py +49 -0
- backend/app/services/language_detector.py +94 -0
- backend/app/services/llm_gateway.py +125 -0
- backend/app/services/llm_operations.py +41 -0
- backend/app/services/llm_run_recorder.py +248 -0
- backend/app/services/model_router.py +92 -0
- backend/app/services/prompts.py +5 -0
- backend/app/services/question_answerer.py +109 -0
- backend/app/services/repo_context.py +192 -0
- backend/app/services/repo_metadata.py +85 -0
- backend/app/services/repo_scanner/__init__.py +42 -0
- backend/app/services/repo_scanner/file_info.py +34 -0
- backend/app/services/repo_scanner/filesystem.py +61 -0
- backend/app/services/repo_scanner/git.py +165 -0
- backend/app/services/repo_scanner/git_ops.py +32 -0
- backend/app/services/repo_scanner/ignore.py +91 -0
- backend/app/services/repo_scanner/models.py +32 -0
- backend/app/services/repo_scanner/scanner.py +135 -0
- backend/app/services/wiki/__init__.py +4 -0
- backend/app/services/wiki/agent_tools.py +117 -0
- backend/app/services/wiki/catalog/__init__.py +298 -0
- backend/app/services/wiki/catalog/source_hints.py +81 -0
- backend/app/services/wiki/catalog_generator.py +261 -0
- backend/app/services/wiki/catalog_planner.py +96 -0
- backend/app/services/wiki/diagrams/__init__.py +271 -0
- backend/app/services/wiki/diagrams/components.py +162 -0
- backend/app/services/wiki/diagrams/data_flow.py +40 -0
- backend/app/services/wiki/diagrams/data_model.py +220 -0
- backend/app/services/wiki/diagrams/models.py +55 -0
- backend/app/services/wiki/diagrams/rendering.py +165 -0
- backend/app/services/wiki/diagrams/sequence.py +45 -0
- backend/app/services/wiki/diagrams/symbol_flow.py +174 -0
- backend/app/services/wiki/generator.py +139 -0
- backend/app/services/wiki/incremental_strategy.py +86 -0
- backend/app/services/wiki/language.py +30 -0
- backend/app/services/wiki/markdown.py +22 -0
- backend/app/services/wiki/mermaid_validation.py +68 -0
- backend/app/services/wiki/page_generator.py +239 -0
- backend/app/services/wiki/page_orchestrator.py +212 -0
- backend/app/services/wiki/page_payload.py +90 -0
- backend/app/services/wiki/page_payload_context.py +151 -0
- backend/app/services/wiki/page_payload_template.py +175 -0
- backend/app/services/wiki/page_validation.py +145 -0
- backend/app/services/wiki/prompts.py +80 -0
- backend/app/services/wiki/sources/__init__.py +41 -0
- backend/app/services/wiki/sources/citations.py +272 -0
- backend/app/services/wiki/sources/rendering.py +279 -0
- backend/app/services/wiki/sources/urls.py +42 -0
- backend/app/services/wiki/translation.py +269 -0
- backend/app/services/wiki/translation_orchestrator.py +264 -0
- backend/app/services/wiki/translation_support.py +175 -0
- backend/app/services/wiki/tree.py +91 -0
- backend/app/services/wiki/utils.py +17 -0
- codewiki-0.1.0.dist-info/METADATA +289 -0
- codewiki-0.1.0.dist-info/RECORD +194 -0
- codewiki-0.1.0.dist-info/WHEEL +5 -0
- codewiki-0.1.0.dist-info/entry_points.txt +2 -0
- codewiki-0.1.0.dist-info/top_level.txt +1 -0
backend/__init__.py
ADDED
backend/app/__init__.py
ADDED
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)]
|
backend/app/api/files.py
ADDED
|
@@ -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
|
+
}
|
backend/app/api/graph.py
ADDED
|
@@ -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"}
|
backend/app/api/repos.py
ADDED
|
@@ -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)
|
backend/app/api/runs.py
ADDED
|
@@ -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
|
+
}
|