wright 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.
- api/__init__.py +0 -0
- api/auth.py +165 -0
- api/chroma_cache.py +98 -0
- api/embedder.py +39 -0
- api/main.py +264 -0
- api/observability.py +74 -0
- api/quota.py +451 -0
- api/rate_limit.py +20 -0
- api/repo_store.py +67 -0
- api/routes/__init__.py +0 -0
- api/routes/auth.py +341 -0
- api/routes/billing.py +303 -0
- api/routes/chat.py +120 -0
- api/routes/coverage.py +120 -0
- api/routes/drift.py +593 -0
- api/routes/fix_pr.py +420 -0
- api/routes/generate.py +200 -0
- api/routes/internal.py +38 -0
- api/routes/llms_txt.py +79 -0
- api/routes/repos.py +854 -0
- api/routes/usage.py +19 -0
- api/routes/webhooks.py +156 -0
- api/tasks/__init__.py +0 -0
- api/tasks/email_tasks.py +413 -0
- api/tasks/ops_alerts.py +198 -0
- api/token_store.py +61 -0
- api/usage_store.py +215 -0
- api/user_store.py +182 -0
- cli/__init__.py +0 -0
- cli/main.py +1125 -0
- core/__init__.py +0 -0
- core/config.py +142 -0
- core/drift/__init__.py +0 -0
- core/drift/drift_detector.py +564 -0
- core/embeddings/__init__.py +0 -0
- core/embeddings/chroma_store.py +142 -0
- core/embeddings/pgvector_store.py +191 -0
- core/embeddings/voyage_embeddings.py +74 -0
- core/llm/__init__.py +0 -0
- core/llm/gateway.py +605 -0
- core/llm/graph.py +179 -0
- core/llm/prompts.py +450 -0
- core/llm/schema.py +31 -0
- core/output/__init__.py +0 -0
- core/output/injector.py +524 -0
- core/output/llms_txt.py +37 -0
- core/output/markdown_writer.py +96 -0
- core/output/openapi_gen.py +77 -0
- core/parser/__init__.py +0 -0
- core/parser/ast_chunker.py +131 -0
- core/parser/cache.py +480 -0
- core/parser/dep_graph.py +89 -0
- core/parser/tree_sitter_parser.py +1111 -0
- core/retrieval/__init__.py +0 -0
- core/retrieval/hybrid_retriever.py +368 -0
- mcp_server/__init__.py +0 -0
- mcp_server/server.py +374 -0
- wright-0.1.0.dist-info/METADATA +557 -0
- wright-0.1.0.dist-info/RECORD +64 -0
- wright-0.1.0.dist-info/WHEEL +5 -0
- wright-0.1.0.dist-info/entry_points.txt +4 -0
- wright-0.1.0.dist-info/licenses/LICENSE +661 -0
- wright-0.1.0.dist-info/licenses/LICENSE-COMMERCIAL.md +43 -0
- wright-0.1.0.dist-info/top_level.txt +4 -0
api/routes/chat.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, Request
|
|
8
|
+
from fastapi.responses import StreamingResponse
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from api.auth import verify_api_key
|
|
12
|
+
from api.quota import check_quota
|
|
13
|
+
from api.rate_limit import limiter
|
|
14
|
+
|
|
15
|
+
router = APIRouter(prefix="/chat", tags=["chat"], dependencies=[Depends(verify_api_key)])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ChatMessage(BaseModel):
|
|
19
|
+
role: str
|
|
20
|
+
content: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChatRequest(BaseModel):
|
|
24
|
+
question: str
|
|
25
|
+
repo_root: str
|
|
26
|
+
conversation_history: list[ChatMessage] = []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.post("")
|
|
30
|
+
@limiter.limit("40/minute")
|
|
31
|
+
async def chat(request: ChatRequest, http_request: Request) -> StreamingResponse:
|
|
32
|
+
"""
|
|
33
|
+
Handles an incoming chat POST request by retrieving relevant code context via hybrid retrieval and streaming AI-generated responses back to the client as server-sent events.
|
|
34
|
+
|
|
35
|
+
This async endpoint initializes the LLM gateway (using Anthropic), a VoyageEmbedder, a ChromaStore vector store, and a HybridRetriever to fetch up to 5 semantically and dependency-relevant code contexts for the user's question. Conversation history is trimmed to the last 20 messages to stay within context limits. The response is streamed as server-sent events, emitting 'token' events for incremental text, 'citations' events for referenced source files, 'followups' events for suggested follow-up questions, and a terminal '[DONE]' sentinel. If embeddings or the vector store are unavailable (e.g., missing API keys or unindexed repository), retrieval is skipped gracefully and the LLM answers from the question alone.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
request (ChatRequest): The chat request object containing the user's question, the repository root path used to locate the Chroma vector store, and the prior conversation history as a list of role/content message pairs.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
StreamingResponse: A Starlette StreamingResponse with media type 'text/event-stream' that emits JSON-encoded server-sent events of types 'token' (incremental LLM text), 'citations' (referenced source files), and 'followups' (suggested follow-up questions), terminated by a '[DONE]' message.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
```
|
|
45
|
+
response = await chat(ChatRequest(question='How does authentication work?', repo_root='/path/to/repo', conversation_history=[]))
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
from api.embedder import get_gateway
|
|
49
|
+
from api.routes.repos import ensure_repo_local
|
|
50
|
+
from api.usage_store import record_event
|
|
51
|
+
|
|
52
|
+
api_key = http_request.headers.get("X-Wright-API-Key", "")
|
|
53
|
+
|
|
54
|
+
# Gate: chat_messages_per_month == 0 on free → 403; >0 enforces monthly cap
|
|
55
|
+
quota = check_quota(api_key, "chat_message", raise_on_blocked=True)
|
|
56
|
+
await ensure_repo_local(request.repo_root)
|
|
57
|
+
gateway = get_gateway()
|
|
58
|
+
history = [{"role": m.role, "content": m.content} for m in request.conversation_history]
|
|
59
|
+
|
|
60
|
+
# Retrieval is best-effort — if embeddings are unavailable (no Voyage/OpenAI
|
|
61
|
+
# key, or repo not yet indexed) we fall back to empty context and Claude
|
|
62
|
+
# answers from the question alone.
|
|
63
|
+
contexts = []
|
|
64
|
+
try:
|
|
65
|
+
from api.chroma_cache import get as get_chroma
|
|
66
|
+
from api.embedder import get_embedder
|
|
67
|
+
from api.routes.repos import get_vector_store
|
|
68
|
+
from core.parser.dep_graph import DependencyGraph
|
|
69
|
+
from core.retrieval.hybrid_retriever import HybridRetriever
|
|
70
|
+
|
|
71
|
+
embedder = get_embedder()
|
|
72
|
+
chroma_path = os.getenv("CHROMA_PATH", os.path.join(request.repo_root, ".wright", "chroma"))
|
|
73
|
+
chroma = get_vector_store(request.repo_root, get_chroma(chroma_path, request.repo_root))
|
|
74
|
+
dep_graph = DependencyGraph()
|
|
75
|
+
retriever = HybridRetriever(chroma, dep_graph, embedder)
|
|
76
|
+
contexts = retriever.retrieve_for_query(request.question, n=5)
|
|
77
|
+
except Exception:
|
|
78
|
+
pass # no embeddings available — Claude answers without code context
|
|
79
|
+
|
|
80
|
+
# Trim history to last 20 messages to stay within context limits
|
|
81
|
+
if len(history) > 20:
|
|
82
|
+
history = history[-20:]
|
|
83
|
+
|
|
84
|
+
async def event_stream():
|
|
85
|
+
tokens_used = 0
|
|
86
|
+
from core.llm.gateway import LLMGateway as _LLMGateway
|
|
87
|
+
|
|
88
|
+
model_used = _LLMGateway.PRIMARY_MODEL
|
|
89
|
+
try:
|
|
90
|
+
if quota.warning:
|
|
91
|
+
yield f"data: {json.dumps({'type': 'quota_warning', 'used': quota.used, 'limit': quota.limit, 'pct': quota.pct, 'upgrade_url': quota.upgrade_url})}\n\n"
|
|
92
|
+
async for kind, payload in gateway.chat_stream(request.question, contexts, history):
|
|
93
|
+
if kind == "token":
|
|
94
|
+
yield f"data: {json.dumps({'type': 'token', 'content': payload})}\n\n"
|
|
95
|
+
elif kind == "citations":
|
|
96
|
+
yield f"data: {json.dumps({'type': 'citations', 'files': payload})}\n\n"
|
|
97
|
+
elif kind == "followups":
|
|
98
|
+
yield f"data: {json.dumps({'type': 'followups', 'questions': payload})}\n\n"
|
|
99
|
+
elif kind == "model":
|
|
100
|
+
model_used = payload
|
|
101
|
+
elif kind == "usage":
|
|
102
|
+
tokens_used = payload
|
|
103
|
+
yield "data: [DONE]\n\n"
|
|
104
|
+
finally:
|
|
105
|
+
# Runs on normal completion AND client disconnect — ensures event is always recorded
|
|
106
|
+
asyncio.create_task(
|
|
107
|
+
asyncio.to_thread(
|
|
108
|
+
record_event,
|
|
109
|
+
api_key,
|
|
110
|
+
"chat_message",
|
|
111
|
+
tokens=tokens_used,
|
|
112
|
+
repo_name=os.path.basename(request.repo_root),
|
|
113
|
+
model=model_used,
|
|
114
|
+
is_fallback=(model_used != _LLMGateway.PRIMARY_MODEL),
|
|
115
|
+
conversation_turns=len(history),
|
|
116
|
+
context_chunks=len(contexts),
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
api/routes/coverage.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, Query, Request
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from api.auth import verify_api_key
|
|
10
|
+
|
|
11
|
+
router = APIRouter(prefix="/coverage", tags=["coverage"], dependencies=[Depends(verify_api_key)])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UndocumentedFunction(BaseModel):
|
|
15
|
+
function_name: str
|
|
16
|
+
file_path: str
|
|
17
|
+
line: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CoverageResponse(BaseModel):
|
|
21
|
+
overall_pct: float
|
|
22
|
+
total: int
|
|
23
|
+
documented: int
|
|
24
|
+
undocumented: list[UndocumentedFunction]
|
|
25
|
+
by_file: dict[str, float]
|
|
26
|
+
by_folder: dict[str, float]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.get("", response_model=CoverageResponse)
|
|
30
|
+
async def get_coverage(
|
|
31
|
+
repo_root: str = Query(..., description="Repository root path"),
|
|
32
|
+
http_request: Request = None,
|
|
33
|
+
) -> CoverageResponse:
|
|
34
|
+
"""
|
|
35
|
+
Scans all Python files under a repository root and returns documentation coverage statistics at the overall, per-file, and per-folder levels.
|
|
36
|
+
|
|
37
|
+
Loads repository configuration via load_config to determine exclude paths, then uses CodeParser to parse every Python file under repo_root. For each parsed file, named functions are counted and checked for existing docstrings to compute total and documented function counts. Undocumented functions are collected with their file path and line number. Coverage percentages are calculated overall, per file, and per folder. If an HTTP request object is provided and contains an X-Wright-API-Key header, the scan event is recorded in the usage store.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
repo_root (str): Absolute or relative path to the repository root directory to scan for Python files and analyze documentation coverage.
|
|
41
|
+
http_request (Request): The incoming FastAPI/Starlette HTTP request object used to extract the X-Wright-API-Key header for usage tracking. Defaults to None if not provided.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
CoverageResponse: A CoverageResponse containing: overall_pct (float, overall documentation coverage percentage), total (int, total function count), documented (int, count of documented functions), undocumented (list of UndocumentedFunction with function_name, file_path, and line), by_file (dict mapping file path to coverage percentage), and by_folder (dict mapping folder path to coverage percentage).
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
```
|
|
48
|
+
coverage = await get_coverage(repo_root="/home/user/projects/my_python_app")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Complexity: O(n*m) time where n is the number of Python files and m is the average number of functions per file; O(n*m) space for storing parsed function data across all files.
|
|
52
|
+
"""
|
|
53
|
+
from api.routes.repos import ensure_repo_local
|
|
54
|
+
from core.config import load_config
|
|
55
|
+
from core.parser.tree_sitter_parser import CodeParser
|
|
56
|
+
|
|
57
|
+
await ensure_repo_local(repo_root)
|
|
58
|
+
|
|
59
|
+
loop = asyncio.get_event_loop()
|
|
60
|
+
config = await loop.run_in_executor(None, load_config, repo_root)
|
|
61
|
+
parser = CodeParser()
|
|
62
|
+
parsed_files = await loop.run_in_executor(
|
|
63
|
+
None, parser.parse_directory, repo_root, config.exclude
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
total = 0
|
|
67
|
+
documented = 0
|
|
68
|
+
undoc_list: list[UndocumentedFunction] = []
|
|
69
|
+
by_file: dict[str, float] = {}
|
|
70
|
+
by_folder: dict[str, tuple[int, int]] = {}
|
|
71
|
+
|
|
72
|
+
for pf in parsed_files:
|
|
73
|
+
funcs = [f for f in pf.functions if f.name != "<anonymous>"]
|
|
74
|
+
for cls in pf.classes:
|
|
75
|
+
funcs += [f for f in cls.methods if f.name != "<anonymous>"]
|
|
76
|
+
file_total = len(funcs)
|
|
77
|
+
file_doc = sum(1 for f in funcs if f.existing_docstring)
|
|
78
|
+
total += file_total
|
|
79
|
+
documented += file_doc
|
|
80
|
+
|
|
81
|
+
if file_total > 0:
|
|
82
|
+
by_file[pf.path] = file_doc / file_total * 100
|
|
83
|
+
|
|
84
|
+
folder = os.path.dirname(pf.path)
|
|
85
|
+
t, d = by_folder.get(folder, (0, 0))
|
|
86
|
+
by_folder[folder] = (t + file_total, d + file_doc)
|
|
87
|
+
|
|
88
|
+
for func in funcs:
|
|
89
|
+
if not func.existing_docstring:
|
|
90
|
+
undoc_list.append(
|
|
91
|
+
UndocumentedFunction(
|
|
92
|
+
function_name=func.name,
|
|
93
|
+
file_path=pf.path,
|
|
94
|
+
line=func.start_line + 1,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
overall_pct = (documented / total * 100) if total else 100.0
|
|
99
|
+
folder_pct = {folder: (d / t * 100 if t > 0 else 100.0) for folder, (t, d) in by_folder.items()}
|
|
100
|
+
|
|
101
|
+
if http_request:
|
|
102
|
+
from api.usage_store import record_event
|
|
103
|
+
|
|
104
|
+
asyncio.create_task(
|
|
105
|
+
asyncio.to_thread(
|
|
106
|
+
record_event,
|
|
107
|
+
http_request.headers.get("X-Wright-API-Key", ""),
|
|
108
|
+
"coverage_scans",
|
|
109
|
+
repo_name=os.path.basename(repo_root),
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return CoverageResponse(
|
|
114
|
+
overall_pct=overall_pct,
|
|
115
|
+
total=total,
|
|
116
|
+
documented=documented,
|
|
117
|
+
undocumented=undoc_list,
|
|
118
|
+
by_file=by_file,
|
|
119
|
+
by_folder=folder_pct,
|
|
120
|
+
)
|