-
Notifications
You must be signed in to change notification settings - Fork 26
feat: add agentic guidelines translation #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
255d258
f790c4e
ca0504d
390467d
e562e70
6331be1
67bbbce
10a2080
3ee6e4d
6b2eda6
843d556
bd06137
df9e64d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
| import jwt | ||
| import structlog | ||
| from cachetools import TTLCache # type: ignore[import-untyped] | ||
| from tenacity import retry, stop_after_attempt, wait_exponential | ||
| from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential | ||
|
|
||
| from src.core.config import config | ||
| from src.core.errors import GitHubGraphQLError | ||
|
|
@@ -129,27 +129,51 @@ async def get_installation_access_token(self, installation_id: int) -> str | Non | |
|
|
||
| async def get_repository( | ||
| self, repo_full_name: str, installation_id: int | None = None, user_token: str | None = None | ||
| ) -> dict[str, Any] | None: | ||
| """Fetch repository metadata (default branch, language, etc.). Supports public access.""" | ||
| ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: | ||
| """ | ||
| Fetch repository metadata. Returns (repo_data, None) on success; | ||
| (None, {"status": int, "message": str}) on failure for meaningful API responses. | ||
| """ | ||
| headers = await self._get_auth_headers( | ||
| installation_id=installation_id, user_token=user_token, allow_anonymous=True | ||
| installation_id=installation_id, user_token=user_token | ||
| ) | ||
| if not headers: | ||
| return None | ||
| return ( | ||
| None, | ||
| {"status": 401, "message": "Authentication required. Provide github_token or installation_id in the request."}, | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| url = f"{config.github.api_base_url}/repos/{repo_full_name}" | ||
| session = await self._get_session() | ||
| async with session.get(url, headers=headers) as response: | ||
| if response.status == 200: | ||
| data = await response.json() | ||
| return cast("dict[str, Any]", data) | ||
| return None | ||
| return cast("dict[str, Any]", data), None | ||
| try: | ||
| body = await response.json() | ||
| gh_message = body.get("message", "") if isinstance(body, dict) else "" | ||
| except Exception: | ||
| gh_message = "" | ||
| if response.status == 404: | ||
| msg = gh_message or "Repository not found or access denied. Check repo name and token permissions." | ||
| return None, {"status": 404, "message": msg} | ||
| if response.status == 403: | ||
| msg = "GitHub API rate limit exceeded. Try again later or provide github_token for higher limits." | ||
| if gh_message and "rate limit" in gh_message.lower(): | ||
| msg = gh_message | ||
| return None, {"status": 403, "message": msg} | ||
| if response.status == 401: | ||
| return ( | ||
| None, | ||
| {"status": 401, "message": gh_message or "Invalid or expired token. Check github_token or installation_id."}, | ||
| ) | ||
| return None, {"status": response.status, "message": gh_message or f"GitHub API returned {response.status}."} | ||
|
Comment on lines
+133
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Use validated models instead of tuple-of-raw-dicts for The new As per coding guidelines, "All agent outputs and external payloads must use validated 🤖 Prompt for AI Agents |
||
|
|
||
| async def list_directory_any_auth( | ||
| self, repo_full_name: str, path: str, installation_id: int | None = None, user_token: str | None = None | ||
| ) -> list[dict[str, Any]]: | ||
| """List directory contents using either installation or user token.""" | ||
| """List directory contents using installation or user token (auth required).""" | ||
| headers = await self._get_auth_headers( | ||
| installation_id=installation_id, user_token=user_token, allow_anonymous=True | ||
| installation_id=installation_id, user_token=user_token | ||
| ) | ||
| if not headers: | ||
| return [] | ||
|
|
@@ -164,24 +188,75 @@ async def list_directory_any_auth( | |
| response.raise_for_status() | ||
| return [] | ||
|
|
||
|
|
||
| async def get_repository_tree( | ||
| self, | ||
| repo_full_name: str, | ||
| ref: str | None = None, | ||
| installation_id: int | None = None, | ||
| user_token: str | None = None, | ||
| recursive: bool = True, | ||
| ) -> list[dict[str, Any]]: | ||
| """Get the tree of a repository. Requires authentication (github_token or installation_id).""" | ||
| headers = await self._get_auth_headers( | ||
| installation_id=installation_id, | ||
| user_token=user_token, | ||
| ) | ||
| if not headers: | ||
| return [] | ||
| ref = ref or "main" | ||
| tree_sha = await self._resolve_tree_sha(repo_full_name, ref, headers) | ||
| if not tree_sha: | ||
| return [] | ||
|
|
||
| url = ( f"{config.github.api_base_url}" | ||
| f"/repos/{repo_full_name}/git/trees/{tree_sha}" | ||
| f"?recursive={recursive}" ) | ||
|
|
||
| session = await self._get_session() | ||
| async with session.get(url, headers=headers) as response: | ||
| if response.status != 200: | ||
| return [] | ||
| data = await response.json() | ||
| return cast("list[dict[str, Any]]", data.get("tree", [])) | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| async def _resolve_tree_sha(self, repo_full_name: str, ref: str, headers: dict[str, str]) -> str | None: | ||
| """Resolve the tree SHA for the given ref (branch, tag, or commit SHA) via the commits API.""" | ||
| session = await self._get_session() | ||
| url = f"{config.github.api_base_url}/repos/{repo_full_name}/commits/{ref}" | ||
| async with session.get(url, headers=headers) as response: | ||
| if response.status != 200: | ||
| return None | ||
| commit_data = await response.json() | ||
| if not isinstance(commit_data, dict): | ||
| return None | ||
| return commit_data.get("commit", {}).get("tree", {}).get("sha") | ||
|
|
||
| async def get_file_content( | ||
| self, repo_full_name: str, file_path: str, installation_id: int | None, user_token: str | None = None | ||
| self, | ||
| repo_full_name: str, | ||
| file_path: str, | ||
| installation_id: int | None, | ||
| user_token: str | None = None, | ||
| ref: str | None = None, | ||
| ) -> str | None: | ||
| """ | ||
| Fetches the content of a file from a repository. Supports anonymous access for public analysis. | ||
| Fetches the content of a file from a repository. Requires authentication (github_token or installation_id). | ||
| When ref is provided (branch name, tag, or commit SHA), returns content at that ref; otherwise uses default branch. | ||
| """ | ||
| headers = await self._get_auth_headers( | ||
| installation_id=installation_id, | ||
| user_token=user_token, | ||
| accept="application/vnd.github.raw", | ||
| allow_anonymous=True, | ||
| ) | ||
| if not headers: | ||
| return None | ||
| url = f"{config.github.api_base_url}/repos/{repo_full_name}/contents/{file_path}" | ||
| params = {"ref": ref} if ref else None | ||
|
|
||
| session = await self._get_session() | ||
| async with session.get(url, headers=headers) as response: | ||
| async with session.get(url, headers=headers, params=params) as response: | ||
| if response.status == 200: | ||
| logger.info(f"Successfully fetched file '{file_path}' from '{repo_full_name}'.") | ||
| return await response.text() | ||
|
|
@@ -1030,7 +1105,6 @@ async def fetch_recent_pull_requests( | |
| headers = await self._get_auth_headers( | ||
| installation_id=installation_id, | ||
| user_token=user_token, | ||
| allow_anonymous=True, # Support public repos | ||
| ) | ||
| if not headers: | ||
| logger.error("pr_fetch_auth_failed", repo=repo_full_name, error_type="auth_error") | ||
|
|
@@ -1115,7 +1189,11 @@ async def fetch_recent_pull_requests( | |
| logger.error("pr_fetch_unexpected_error", repo=repo_full_name, error_type="unknown_error", error=str(e)) | ||
| return [] | ||
|
|
||
| @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) | ||
| @retry( | ||
| retry=retry_if_exception_type(aiohttp.ClientError), | ||
| stop=stop_after_attempt(3), | ||
| wait=wait_exponential(multiplier=1, min=4, max=10), | ||
| ) | ||
| async def execute_graphql( | ||
| self, query: str, variables: dict[str, Any], user_token: str | None = None, installation_id: int | None = None | ||
| ) -> dict[str, Any]: | ||
|
|
@@ -1139,18 +1217,17 @@ async def execute_graphql( | |
| url = f"{config.github.api_base_url}/graphql" | ||
| payload = {"query": query, "variables": variables} | ||
|
|
||
| # Get appropriate headers (can be anonymous for public data or authenticated) | ||
| # Priority: user_token > installation_id > anonymous (if allowed) | ||
| # Get appropriate headers (auth required: user_token or installation_id) | ||
| headers = await self._get_auth_headers( | ||
| user_token=user_token, installation_id=installation_id, allow_anonymous=True | ||
| user_token=user_token, installation_id=installation_id | ||
| ) | ||
| if not headers: | ||
| # Fallback or error? GraphQL usually demands auth. | ||
| # If we have no headers, we likely can't query GraphQL successfully for many fields. | ||
| # We'll try with empty headers if that's what _get_auth_headers returns (it returns None on failure). | ||
| # If None, we can't proceed. | ||
| logger.error("GraphQL execution failed: No authentication headers available.") | ||
| raise Exception("Authentication required for GraphQL query.") | ||
| raise PermissionError("Authentication required for GraphQL query.") | ||
|
|
||
| start_time = time.time() | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.