Hyeonseo commited on
Commit
b0cec26
·
1 Parent(s): 432715c

Convert MCP server submodules to regular directories

Browse files
external/mcp-servers/hf-translation-docs-explorer DELETED
@@ -1 +0,0 @@
1
- Subproject commit 45883c3faf36a9abcf03ede91a12ed4c8f3ab1cc
 
 
external/mcp-servers/hf-translation-docs-explorer/.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
external/mcp-servers/hf-translation-docs-explorer/README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Traslation File Explorer
3
+ emoji: ⚡
4
+ colorFrom: red
5
+ colorTo: pink
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
external/mcp-servers/hf-translation-docs-explorer/adapters.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, List
4
+
5
+ import requests
6
+
7
+ from setting import SETTINGS
8
+
9
+
10
+ def _build_auth_headers() -> Dict[str, str]:
11
+ """
12
+ GitHub 호출용 Authorization 헤더 생성.
13
+ - 우선순위: SETTINGS.github_token → (fallback) 환경변수 GITHUB_TOKEN
14
+ """
15
+ token = SETTINGS.github_token
16
+ if not token:
17
+ # 환경변수 직접 조회
18
+ import os
19
+ token = os.environ.get("GITHUB_TOKEN", "")
20
+
21
+ if not token:
22
+ return {}
23
+ return {"Authorization": f"token {token}"}
24
+
25
+
26
+ def fetch_document_paths(api_url: str) -> List[str]:
27
+ """
28
+ GitHub git/trees API에서 blob 경로 목록만 추출.
29
+
30
+ Parameters
31
+ ----------
32
+ api_url : str
33
+ 예: https://api.github.com/repos/huggingface/transformers/git/trees/main?recursive=1
34
+ """
35
+ response = requests.get(
36
+ api_url,
37
+ headers=_build_auth_headers(),
38
+ timeout=SETTINGS.request_timeout_seconds,
39
+ )
40
+
41
+ if response.status_code == 403 and "rate limit" in response.text.lower():
42
+ raise RuntimeError(
43
+ "GitHub API rate limit exceeded. Provide a GITHUB_TOKEN to continue."
44
+ )
45
+
46
+ response.raise_for_status()
47
+ tree = response.json().get("tree", [])
48
+ return [item["path"] for item in tree if item.get("type") == "blob"]
external/mcp-servers/hf-translation-docs-explorer/app.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+
6
+ import gradio as gr
7
+
8
+ from services import get_available_projects, LANGUAGE_CHOICES
9
+ from tools import list_projects, search_files, list_missing_files
10
+ from setting import SETTINGS
11
+
12
+
13
+ def ensure_mcp_support() -> None:
14
+ """Verify that ``gradio[mcp]`` is installed and enable the MCP server flag."""
15
+ try:
16
+ import gradio.mcp # noqa: F401
17
+ except ImportError as exc: # pragma: no cover - runtime guard
18
+ raise RuntimeError("Install gradio[mcp] before launching this module.") from exc
19
+ os.environ.setdefault("GRADIO_MCP_SERVER", "true")
20
+
21
+
22
+ def build_demo() -> gr.Blocks:
23
+ """Create a lightweight Gradio Blocks UI for exercising the MCP tools."""
24
+ projects = get_available_projects()
25
+ languages = LANGUAGE_CHOICES[:]
26
+
27
+ with gr.Blocks(title=SETTINGS.ui_title) as demo:
28
+ gr.Markdown("# Translation MCP Server\nTry the MCP tools exposed below.")
29
+
30
+ # --- 1) Project catalog ---
31
+ with gr.Tab("Project catalog"):
32
+ catalog_output = gr.JSON(label="catalog")
33
+ gr.Button("Fetch").click(
34
+ fn=list_projects,
35
+ inputs=[], # 인자 없음
36
+ outputs=catalog_output,
37
+ api_name="translation_project_catalog",
38
+ )
39
+
40
+ # --- 2) File search (report + candidates) ---
41
+ with gr.Tab("File search"):
42
+ project_input = gr.Dropdown(
43
+ choices=projects,
44
+ label="Project",
45
+ value=projects[0] if projects else "",
46
+ )
47
+ lang_input = gr.Dropdown(
48
+ choices=languages,
49
+ label="Language",
50
+ value=SETTINGS.default_language,
51
+ )
52
+ limit_input = gr.Number(
53
+ label="Limit",
54
+ value=SETTINGS.default_limit,
55
+ minimum=1,
56
+ )
57
+ include_report = gr.Checkbox(
58
+ label="Include status report",
59
+ value=True,
60
+ )
61
+
62
+ search_output = gr.JSON(label="search result")
63
+ gr.Button("Search").click(
64
+ fn=search_files,
65
+ inputs=[project_input, lang_input, limit_input, include_report],
66
+ outputs=search_output,
67
+ api_name="translation_file_search",
68
+ )
69
+
70
+ # --- 3) Missing docs only ---
71
+ with gr.Tab("Missing docs"):
72
+ missing_project = gr.Dropdown(
73
+ choices=projects,
74
+ label="Project",
75
+ value=projects[0] if projects else "",
76
+ )
77
+ missing_lang = gr.Dropdown(
78
+ choices=languages,
79
+ label="Language",
80
+ value=SETTINGS.default_language,
81
+ )
82
+ missing_limit = gr.Number(
83
+ label="Limit",
84
+ value=max(SETTINGS.default_limit, 20),
85
+ minimum=1,
86
+ )
87
+
88
+ missing_output = gr.JSON(label="missing files")
89
+ gr.Button("List missing").click(
90
+ fn=list_missing_files,
91
+ inputs=[missing_project, missing_lang, missing_limit],
92
+ outputs=missing_output,
93
+ api_name="translation_missing_list",
94
+ )
95
+
96
+ return demo
97
+
98
+
99
+ def _parse_args(argv=None) -> argparse.Namespace:
100
+ """Parse CLI arguments used for local or Space deployments."""
101
+ parser = argparse.ArgumentParser(description="Launch the translation MCP demo.")
102
+
103
+ parser.add_argument(
104
+ "--as-space",
105
+ action="store_true",
106
+ help="Use Hugging Face Space defaults.",
107
+ )
108
+ parser.add_argument(
109
+ "--share",
110
+ action="store_true",
111
+ help="Create a public share link.",
112
+ )
113
+ parser.add_argument(
114
+ "--no-queue",
115
+ dest="queue",
116
+ action="store_false",
117
+ help="Disable the request queue.",
118
+ )
119
+ parser.set_defaults(queue=True)
120
+
121
+ return parser.parse_args(argv)
122
+
123
+
124
+ def main(argv=None) -> None:
125
+ """Launch the Gradio app with MCP server support enabled."""
126
+ args = _parse_args(argv)
127
+
128
+ ensure_mcp_support()
129
+
130
+ launch_kwargs = {"mcp_server": True}
131
+
132
+ if args.as_space or os.environ.get("SPACE_ID"):
133
+ launch_kwargs.update(
134
+ {
135
+ "server_name": "0.0.0.0",
136
+ "server_port": int(os.environ.get("PORT", "7860")),
137
+ "show_api": False,
138
+ }
139
+ )
140
+ else:
141
+ launch_kwargs["show_api"] = True
142
+
143
+ if args.share:
144
+ launch_kwargs["share"] = True
145
+
146
+ demo = build_demo()
147
+ app = demo.queue() if args.queue else demo
148
+ app.launch(**launch_kwargs)
149
+
150
+
151
+ if __name__ == "__main__": # pragma: no cover - manual execution helper
152
+ main()
external/mcp-servers/hf-translation-docs-explorer/configs/defaults.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ github:
2
+ token: "" # 기본값: 환경변수 GITHUB_TOKEN 사용 권장
3
+ request_timeout_seconds: 30
4
+
5
+ translation:
6
+ default_language: "ko" # 기본 타겟 언어
7
+ default_limit: 5 # 기본 검색/누락 파일 개수
8
+
9
+ ui:
10
+ title: "Translation Docs Search MCP Server"
external/mcp-servers/hf-translation-docs-explorer/pyproject.toml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "translation-file-explorer-mcp"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.10"
5
+ dependencies = [
6
+ "gradio[mcp]>=5.33.0",
7
+ "pydantic>=2.7.0",
8
+ "requests>=2.31.0",
9
+ "pyyaml>=6.0.1",
10
+ ]
11
+
12
+ [tool.ruff]
13
+ line-length = 100
external/mcp-servers/hf-translation-docs-explorer/requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio[mcp]==5.33.0
2
+ requests
3
+ pydantic
4
+ langchain-anthropic
5
+ python-dotenv
6
+ langchain
7
+ PyGithub
8
+ langchain-core
9
+ langchain-community
10
+ boto3
11
+ PyYAML
external/mcp-servers/hf-translation-docs-explorer/services.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Iterable, List, Tuple
6
+
7
+ from adapters import fetch_document_paths
8
+ from setting import SETTINGS
9
+
10
+
11
+ # Gradio / UI 에 노출할 언어 선택지
12
+ LANGUAGE_CHOICES: List[str] = [
13
+ "ko",
14
+ "ja",
15
+ "zh",
16
+ "fr",
17
+ "de",
18
+ ]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Project:
23
+ """Store the minimum metadata required for documentation lookups."""
24
+
25
+ slug: str
26
+ name: str
27
+ repo_url: str
28
+ docs_path: str
29
+ tree_api_url: str
30
+
31
+ @property
32
+ def repo_path(self) -> str:
33
+ """Return the ``owner/repo`` identifier for GitHub API requests."""
34
+ return self.repo_url.replace("https://github.com/", "")
35
+
36
+
37
+ # 지원 프로젝트 정의
38
+ PROJECTS: Dict[str, Project] = {
39
+ "transformers": Project(
40
+ slug="transformers",
41
+ name="Transformers",
42
+ repo_url="https://github.com/huggingface/transformers",
43
+ docs_path="docs/source",
44
+ tree_api_url=(
45
+ "https://api.github.com/repos/huggingface/transformers/git/trees/main?recursive=1"
46
+ ),
47
+ ),
48
+ "smolagents": Project(
49
+ slug="smolagents",
50
+ name="SmolAgents",
51
+ repo_url="https://github.com/huggingface/smolagents",
52
+ docs_path="docs/source",
53
+ tree_api_url=(
54
+ "https://api.github.com/repos/huggingface/smolagents/git/trees/main?recursive=1"
55
+ ),
56
+ ),
57
+ }
58
+
59
+
60
+ def get_available_projects() -> List[str]:
61
+ """Return the list of project slugs supported by this module."""
62
+ return sorted(PROJECTS.keys())
63
+
64
+
65
+ def _iter_english_docs(all_docs: Iterable[str], docs_root: str) -> Iterable[Path]:
66
+ """Yield English documentation files as ``Path`` objects."""
67
+ english_root = Path(docs_root) / "en"
68
+
69
+ for doc_path in all_docs:
70
+ if not doc_path.endswith(".md"):
71
+ continue
72
+
73
+ path = Path(doc_path)
74
+ try:
75
+ # en/ 아래에 있는지 필터링
76
+ path.relative_to(english_root)
77
+ except ValueError:
78
+ continue
79
+
80
+ yield path
81
+
82
+
83
+ def _compute_missing_translations(
84
+ project_key: str,
85
+ language: str,
86
+ limit: int,
87
+ ) -> Tuple[str, List[str], Project]:
88
+ """
89
+ 영어 기준으로 누락 번역 파일을 계산하고,
90
+ 마크다운 요약 리포트 + 누락 경로 리스트 + Project 메타데이터를 반환.
91
+ """
92
+ project = PROJECTS[project_key]
93
+
94
+ all_paths = fetch_document_paths(project.tree_api_url)
95
+ english_docs = list(_iter_english_docs(all_paths, project.docs_path))
96
+ english_total = len(english_docs)
97
+
98
+ missing: List[str] = []
99
+ docs_set = set(all_paths)
100
+
101
+ for english_doc in english_docs:
102
+ relative = english_doc.relative_to(Path(project.docs_path) / "en")
103
+ translated_path = str(Path(project.docs_path) / language / relative)
104
+
105
+ if translated_path not in docs_set:
106
+ # 누락된 경우: 기준은 영어 경로(en/...)
107
+ missing.append(str(english_doc))
108
+ if len(missing) >= limit:
109
+ break
110
+
111
+ missing_count = len(missing)
112
+ percentage = (missing_count / english_total * 100) if english_total else 0.0
113
+
114
+ report = (
115
+ "| Item | Count | Percentage |\n"
116
+ "|------|-------|------------|\n"
117
+ f"| English docs | {english_total} | - |\n"
118
+ f"| Missing translations | {missing_count} | {percentage:.2f}% |"
119
+ )
120
+
121
+ return report, missing, project
122
+
123
+
124
+ def build_project_catalog(default: str | None) -> Dict[str, Any]:
125
+ """Build the project catalog payload (API-neutral, pure logic)."""
126
+ slugs = get_available_projects()
127
+ default = default if default in slugs else None
128
+
129
+ return {
130
+ "type": "translation.project_list",
131
+ "projects": [
132
+ {
133
+ "slug": slug,
134
+ "display_name": PROJECTS[slug].name,
135
+ "repo_url": PROJECTS[slug].repo_url,
136
+ "docs_path": PROJECTS[slug].docs_path,
137
+ }
138
+ for slug in slugs
139
+ ],
140
+ "default_project": default,
141
+ "total_projects": len(slugs),
142
+ }
143
+
144
+
145
+ def build_search_response(
146
+ project: str,
147
+ lang: str,
148
+ limit: int,
149
+ include_status_report: bool,
150
+ ) -> Dict[str, Any]:
151
+ """
152
+ 누락 번역 파일 후보 + (선택) 상태 리포트를 포함한 검색 응답.
153
+ MCP / Gradio 에서 사용 가능한 JSON 형태.
154
+ """
155
+ project = project.strip()
156
+ lang = lang.strip()
157
+ limit = max(1, int(limit))
158
+
159
+ project_config = PROJECTS[project]
160
+
161
+ status_report, candidate_paths, project_config = _compute_missing_translations(
162
+ project_key=project,
163
+ language=lang,
164
+ limit=limit,
165
+ )
166
+
167
+ repo_url = project_config.repo_url.rstrip("/")
168
+
169
+ return {
170
+ "type": "translation.search.response",
171
+ "request": {
172
+ "type": "translation.search.request",
173
+ "project": project,
174
+ "target_language": lang,
175
+ "limit": limit,
176
+ "include_status_report": include_status_report,
177
+ },
178
+ "files": [
179
+ {
180
+ "rank": index,
181
+ "path": path,
182
+ "repo_url": f"{repo_url}/blob/main/{path}",
183
+ "metadata": {
184
+ "project": project,
185
+ "target_language": lang,
186
+ "docs_path": project_config.docs_path,
187
+ },
188
+ }
189
+ for index, path in enumerate(candidate_paths, start=1)
190
+ ],
191
+ "total_candidates": len(candidate_paths),
192
+ "status_report": status_report if include_status_report else None,
193
+ }
194
+
195
+
196
+ def build_missing_list_response(
197
+ project: str,
198
+ lang: str,
199
+ limit: int,
200
+ ) -> Dict[str, Any]:
201
+ """
202
+ 누락 번역 파일 목록만 제공하는 응답(JSON).
203
+ """
204
+ project = project.strip()
205
+ lang = lang.strip()
206
+ limit_int = max(1, int(limit))
207
+
208
+ status_report, missing_paths, project_config = _compute_missing_translations(
209
+ project_key=project,
210
+ language=lang,
211
+ limit=limit_int,
212
+ )
213
+
214
+ repo_url = project_config.repo_url.rstrip("/")
215
+
216
+ return {
217
+ "type": "translation.missing_list",
218
+ "project": project,
219
+ "target_language": lang,
220
+ "limit": limit_int,
221
+ "count": len(missing_paths),
222
+ "files": [
223
+ {
224
+ "rank": index,
225
+ "path": path,
226
+ "repo_url": f"{repo_url}/blob/main/{path}",
227
+ "metadata": {
228
+ "project": project,
229
+ "target_language": lang,
230
+ "docs_path": project_config.docs_path,
231
+ },
232
+ }
233
+ for index, path in enumerate(missing_paths, start=1)
234
+ ],
235
+ "status_report": status_report, # 필요 없다면 제거 가능
236
+ }
external/mcp-servers/hf-translation-docs-explorer/setting.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Dict
6
+
7
+ import os
8
+
9
+ try:
10
+ import yaml # type: ignore
11
+ except Exception:
12
+ yaml = None
13
+
14
+
15
+ @dataclass
16
+ class AppSettings:
17
+ github_token: str = ""
18
+ request_timeout_seconds: int = 30
19
+ default_language: str = "ko"
20
+ default_limit: int = 5
21
+ ui_title: str = "Translation MCP Server"
22
+
23
+
24
+ def _load_yaml(path: Path) -> Dict[str, Any]:
25
+ if not path.is_file():
26
+ return {}
27
+ if yaml is None:
28
+ return {}
29
+ with path.open("r", encoding="utf-8") as f:
30
+ data = yaml.safe_load(f) or {}
31
+ return data if isinstance(data, dict) else {}
32
+
33
+
34
+ def load_settings(config_path: str = "configs/default.yaml") -> AppSettings:
35
+ cfg = _load_yaml(Path(config_path))
36
+
37
+ github_cfg = cfg.get("github", {}) if isinstance(cfg.get("github"), dict) else {}
38
+ trans_cfg = cfg.get("translation", {}) if isinstance(cfg.get("translation"), dict) else {}
39
+ ui_cfg = cfg.get("ui", {}) if isinstance(cfg.get("ui"), dict) else {}
40
+
41
+ # ENV > YAML
42
+ github_token = os.getenv("GITHUB_TOKEN", github_cfg.get("token", ""))
43
+ request_timeout_seconds = int(
44
+ os.getenv("REQUEST_TIMEOUT_SECONDS", github_cfg.get("request_timeout_seconds", 30))
45
+ )
46
+ default_language = os.getenv("DEFAULT_LANGUAGE", trans_cfg.get("default_language", "ko"))
47
+ default_limit = int(
48
+ os.getenv("DEFAULT_LIMIT", trans_cfg.get("default_limit", 5))
49
+ )
50
+ ui_title = ui_cfg.get("title", "Translation MCP Server")
51
+
52
+ return AppSettings(
53
+ github_token=github_token,
54
+ request_timeout_seconds=request_timeout_seconds,
55
+ default_language=default_language,
56
+ default_limit=default_limit,
57
+ ui_title=ui_title,
58
+ )
59
+
60
+
61
+ # 전역 설정 인스턴스
62
+ SETTINGS: AppSettings = load_settings()
external/mcp-servers/hf-translation-docs-explorer/tools.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+ from services import (
6
+ build_project_catalog,
7
+ build_search_response,
8
+ build_missing_list_response,
9
+ )
10
+
11
+
12
+ def list_projects() -> Dict[str, Any]:
13
+ """
14
+ Gradio + MCP에서 사용되는 'translation_project_catalog' 엔드포인트.
15
+ 입력값 없이 전체 프로젝트 카탈로그를 반환한다.
16
+ """
17
+ return build_project_catalog(default=None)
18
+
19
+
20
+ def search_files(
21
+ project: str,
22
+ lang: str,
23
+ limit: float | int,
24
+ include_status_report: bool,
25
+ ) -> Dict[str, Any]:
26
+ """
27
+ Gradio + MCP에서 사용되는 'translation_file_search' 엔드포인트.
28
+ """
29
+ return build_search_response(
30
+ project=project,
31
+ lang=lang,
32
+ limit=int(limit or 1),
33
+ include_status_report=bool(include_status_report),
34
+ )
35
+
36
+
37
+ def list_missing_files(
38
+ project: str,
39
+ lang: str,
40
+ limit: float | int,
41
+ ) -> Dict[str, Any]:
42
+ """
43
+ Gradio + MCP에서 사용되는 'translation_missing_list' 엔드포인트.
44
+ 누락 파일 리스트만 반환.
45
+ """
46
+ return build_missing_list_response(
47
+ project=project,
48
+ lang=lang,
49
+ limit=int(limit or 1),
50
+ )
external/mcp-servers/hf-translation-reviewer DELETED
@@ -1 +0,0 @@
1
- Subproject commit d3b82f5b7eba8b3121d9f49792908edb59758e47
 
 
external/mcp-servers/hf-translation-reviewer/.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
external/mcp-servers/hf-translation-reviewer/README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: LLM Translation Reviewer
3
+ emoji: 🦀
4
+ colorFrom: blue
5
+ colorTo: gray
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
external/mcp-servers/hf-translation-reviewer/adapters.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from typing import Dict, List, Optional
5
+
6
+ from urllib.parse import urlparse
7
+
8
+ import requests
9
+
10
+ from setting import SETTINGS
11
+
12
+ # Optional provider SDKs
13
+ try:
14
+ import openai # type: ignore
15
+ except Exception:
16
+ openai = None
17
+
18
+ try:
19
+ import anthropic # type: ignore
20
+ except Exception:
21
+ anthropic = None
22
+
23
+ try:
24
+ import google.generativeai as genai # type: ignore
25
+ except Exception:
26
+ genai = None
27
+
28
+
29
+ # ---------------- GitHub HTTP adapters -----------------
30
+
31
+
32
+ def github_request(
33
+ url: str,
34
+ token: str,
35
+ params: Optional[Dict[str, str]] = None,
36
+ ) -> Dict:
37
+ headers = {
38
+ "Accept": "application/vnd.github.v3+json",
39
+ "Authorization": f"token {token}",
40
+ }
41
+ response = requests.get(url, headers=headers, params=params, timeout=30)
42
+ if response.status_code == 404:
43
+ raise FileNotFoundError(f"GitHub resource not found: {url}")
44
+ if response.status_code == 401:
45
+ raise PermissionError("GitHub token is invalid or lacks necessary scopes.")
46
+ if response.status_code >= 400:
47
+ raise RuntimeError(
48
+ f"GitHub API request failed with status {response.status_code}: {response.text}"
49
+ )
50
+ return response.json()
51
+
52
+
53
+ def fetch_file_from_pr(
54
+ repo_name: str,
55
+ pr_number: int,
56
+ path: str,
57
+ head_sha: str,
58
+ github_token: str,
59
+ ) -> str:
60
+ url = f"{SETTINGS.github_api_base}/repos/{repo_name}/contents/{path}"
61
+ data = github_request(url, github_token, params={"ref": head_sha})
62
+ content = data.get("content")
63
+ encoding = data.get("encoding")
64
+ if content is None or encoding != "base64":
65
+ raise ValueError(
66
+ f"Unexpected content response for '{path}' (encoding={encoding!r})."
67
+ )
68
+ decoded = base64.b64decode(content)
69
+ try:
70
+ return decoded.decode("utf-8")
71
+ except UnicodeDecodeError as exc:
72
+ raise ValueError(
73
+ f"File '{path}' in PR {pr_number} is not valid UTF-8 text"
74
+ ) from exc
75
+
76
+
77
+ # ---------------- LLM provider adapters -----------------
78
+
79
+
80
+ def call_openai(
81
+ token: str,
82
+ system_prompt: str,
83
+ user_prompt: str,
84
+ model_name: str = "gpt-5",
85
+ ) -> str:
86
+ if openai is None:
87
+ raise RuntimeError("openai package not installed. Install with `pip install openai`.")
88
+ client = openai.OpenAI(api_key=token)
89
+ params = {
90
+ "model": model_name,
91
+ "messages": [
92
+ {"role": "system", "content": system_prompt},
93
+ {"role": "user", "content": user_prompt},
94
+ ],
95
+ }
96
+ # Some models (e.g., gpt-5) may not allow custom temperature.
97
+ if model_name not in {"gpt-5"}:
98
+ params["temperature"] = 0.2
99
+ response = client.chat.completions.create(**params)
100
+ return response.choices[0].message.content.strip()
101
+
102
+
103
+ def call_anthropic(
104
+ token: str,
105
+ system_prompt: str,
106
+ user_prompt: str,
107
+ model_name: str = "claude-3-5-sonnet-20240620",
108
+ ) -> str:
109
+ if anthropic is None:
110
+ raise RuntimeError("anthropic package not installed. Install with `pip install anthropic`.")
111
+ client = anthropic.Anthropic(api_key=token)
112
+ response = client.messages.create(
113
+ model=model_name,
114
+ system=system_prompt,
115
+ max_tokens=1500,
116
+ temperature=0.2,
117
+ messages=[{"role": "user", "content": user_prompt}],
118
+ )
119
+ return "".join(block.text for block in response.content if hasattr(block, "text")).strip()
120
+
121
+
122
+ def call_gemini(
123
+ token: str,
124
+ system_prompt: str,
125
+ user_prompt: str,
126
+ model_name: str = "gemini-1.5-pro",
127
+ ) -> str:
128
+ if genai is None:
129
+ raise RuntimeError("google-generativeai package not installed. Install with `pip install google-generativeai`.")
130
+ genai.configure(api_key=token)
131
+ model = genai.GenerativeModel(model_name)
132
+ prompt = f"{system_prompt}\n\n{user_prompt}"
133
+ response = model.generate_content(prompt, generation_config={"temperature": 0.2})
134
+ return response.text.strip()
135
+
136
+
137
+ PROVIDERS = {
138
+ "openai": call_openai,
139
+ "anthropic": call_anthropic,
140
+ "gemini": call_gemini,
141
+ }
142
+
143
+
144
+ def dispatch_review(
145
+ provider: str,
146
+ token: str,
147
+ system_prompt: str,
148
+ user_prompt: str,
149
+ model_name: str,
150
+ ) -> str:
151
+ if provider not in PROVIDERS:
152
+ raise ValueError(f"Unknown provider '{provider}'. Choose from: {', '.join(PROVIDERS)}")
153
+ return PROVIDERS[provider](token, system_prompt, user_prompt, model_name)
external/mcp-servers/hf-translation-reviewer/app.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gradio + MCP server app for LLM translation review on GitHub PRs.
4
+
5
+ - UI만 담당하고, 실제 로직은 tools/services/adapters 로 분리.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ import gradio as gr
13
+
14
+ from setting import SETTINGS
15
+ from tools import (
16
+ tool_prepare,
17
+ tool_review_and_emit,
18
+ tool_submit_review,
19
+ tool_end_to_end,
20
+ )
21
+
22
+
23
+ def build_ui() -> gr.Blocks:
24
+ with gr.Blocks(title=SETTINGS.ui_title) as demo:
25
+ gr.Markdown(
26
+ "# LLM Translation Reviewer for GitHub PRs (MCP-enabled)\n"
27
+ "Only **PR URL** + fields below are required. Repo/PR number are parsed."
28
+ )
29
+
30
+ # 공통 입력 영역
31
+ with gr.Row():
32
+ pr_url = gr.Textbox(
33
+ label="PR URL",
34
+ placeholder="https://github.com/owner/repo/pull/123",
35
+ scale=2,
36
+ )
37
+ provider = gr.Dropdown(
38
+ label="Provider",
39
+ choices=["openai", "anthropic", "gemini"],
40
+ value=SETTINGS.default_provider,
41
+ )
42
+ model_name = gr.Textbox(
43
+ label="Model name",
44
+ value=SETTINGS.default_model,
45
+ placeholder=(
46
+ "e.g., gpt-5 / gpt-4o / claude-3-5-sonnet-20240620 / gemini-1.5-pro"
47
+ ),
48
+ )
49
+ with gr.Row():
50
+ provider_token = gr.Textbox(
51
+ label="Provider API Token",
52
+ type="password",
53
+ )
54
+ github_token = gr.Textbox(
55
+ label="GitHub Token",
56
+ type="password",
57
+ )
58
+ with gr.Row():
59
+ original_path = gr.Textbox(
60
+ label="Original File Path (in repo)",
61
+ placeholder="docs/source/en/xxx.md",
62
+ )
63
+ translated_path = gr.Textbox(
64
+ label="Translated File Path (in repo)",
65
+ placeholder="docs/source/ko/xxx.md",
66
+ )
67
+
68
+ gr.Markdown("---")
69
+
70
+ # Tool 1: Prepare
71
+ with gr.Accordion(
72
+ "Tool 1: Prepare (Fetch Files + Build Prompts)", open=False
73
+ ):
74
+ prepare_btn = gr.Button("tool_prepare")
75
+ prepare_out = gr.JSON(label="Prepare result (files + prompts)")
76
+
77
+ prepare_btn.click(
78
+ fn=tool_prepare,
79
+ inputs=[github_token, pr_url, original_path, translated_path],
80
+ outputs=[prepare_out],
81
+ )
82
+
83
+ # Tool 2: Review + Emit Payload
84
+ with gr.Accordion("Tool 2: Review + Emit Payload", open=False):
85
+ review_btn = gr.Button("tool_review_and_emit")
86
+
87
+ original_text = gr.Textbox(
88
+ label="Original (for review)",
89
+ lines=6,
90
+ )
91
+ translated_text = gr.Textbox(
92
+ label="Translated (for review)",
93
+ lines=10,
94
+ )
95
+
96
+ review_out = gr.JSON(
97
+ label="Review result (verdict/summary/comments/event)"
98
+ )
99
+ payload_out = gr.JSON(label="Payload JSON (for GitHub)")
100
+
101
+ def _review_emit_proxy(
102
+ provider_: str,
103
+ provider_token_: str,
104
+ model_name_: str,
105
+ pr_url_: str,
106
+ translated_path_: str,
107
+ original_text_: str,
108
+ translated_text_: str,
109
+ ):
110
+ result = tool_review_and_emit(
111
+ provider=provider_,
112
+ provider_token=provider_token_,
113
+ model_name=model_name_,
114
+ pr_url=pr_url_,
115
+ translated_path=translated_path_,
116
+ original=original_text_,
117
+ translated=translated_text_,
118
+ )
119
+ return result, result.get("payload", {})
120
+
121
+ review_btn.click(
122
+ fn=_review_emit_proxy,
123
+ inputs=[
124
+ provider,
125
+ provider_token,
126
+ model_name,
127
+ pr_url,
128
+ translated_path,
129
+ original_text,
130
+ translated_text,
131
+ ],
132
+ outputs=[review_out, payload_out],
133
+ )
134
+
135
+ # Tool 3: Submit Review
136
+ with gr.Accordion("Tool 3: Submit Review", open=False):
137
+ submit_btn = gr.Button("tool_submit_review")
138
+ payload_in = gr.Textbox(
139
+ label="Payload or Review JSON (from Tool 2)",
140
+ lines=6,
141
+ )
142
+ submit_out = gr.JSON(label="Submission result")
143
+
144
+ def _submit_proxy(
145
+ github_token_: str,
146
+ pr_url_: str,
147
+ translated_path_: str,
148
+ payload_json_: str,
149
+ ):
150
+ try:
151
+ payload_obj = json.loads(payload_json_) if payload_json_ else {}
152
+ except Exception as e:
153
+ raise ValueError(f"Invalid JSON: {e}")
154
+ return tool_submit_review(
155
+ github_token=github_token_,
156
+ pr_url=pr_url_,
157
+ translated_path=translated_path_,
158
+ payload_or_review=payload_obj,
159
+ allow_self_request_changes=True,
160
+ )
161
+
162
+ submit_btn.click(
163
+ fn=_submit_proxy,
164
+ inputs=[github_token, pr_url, translated_path, payload_in],
165
+ outputs=[submit_out],
166
+ )
167
+
168
+ gr.Markdown("---")
169
+
170
+ # Tool 4: End-to-End
171
+ with gr.Accordion("Tool 4: End-to-End", open=True):
172
+ e2e_btn = gr.Button("tool_end_to_end")
173
+ save_review = gr.Checkbox(
174
+ label="Save review JSON to file", value=True
175
+ )
176
+ save_path = gr.Textbox(
177
+ label="Save path", value="review.json"
178
+ )
179
+ submit_flag = gr.Checkbox(
180
+ label="Submit to GitHub", value=False
181
+ )
182
+ e2e_out = gr.JSON(label="E2E result")
183
+
184
+ e2e_btn.click(
185
+ fn=tool_end_to_end,
186
+ inputs=[
187
+ provider,
188
+ provider_token,
189
+ model_name,
190
+ github_token,
191
+ pr_url,
192
+ original_path,
193
+ translated_path,
194
+ save_review,
195
+ save_path,
196
+ submit_flag,
197
+ ],
198
+ outputs=[e2e_out],
199
+ )
200
+
201
+ gr.Markdown(
202
+ """
203
+ **Notes**
204
+ - Tool 1: PR에서 파일을 읽고 프롬프트까지 준비합니다.
205
+ - Tool 2: LLM으로 리뷰한 뒤, GitHub 리뷰 payload까지 생성합니다.
206
+ - Tool 3: Tool 2에서 만든 payload JSON을 그대로 넣고 GitHub에 전송합니다.
207
+ - Tool 4: 파일 로드부터 리뷰/저장/제출까지 한 번에 처리하는 end-to-end 툴입니다.
208
+ - `launch(mcp_server=True)` 이므로 각 `tool_*` 버튼은 MCP 툴로도 사용 가능합니다.
209
+ """
210
+ )
211
+ return demo
212
+
213
+
214
+ if __name__ == "__main__":
215
+ ui = build_ui()
216
+ ui.launch(
217
+ share=SETTINGS.ui_share,
218
+ mcp_server=SETTINGS.ui_launch_mcp_server,
219
+ )
external/mcp-servers/hf-translation-reviewer/configs/default.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ provider:
2
+ default: "openai"
3
+ model: "gpt-5"
4
+
5
+ github:
6
+ api_base: "https://api.github.com"
7
+
8
+ ui:
9
+ title: "LLM Translation Reviewer (PR) — MCP Tools"
10
+ share: true
11
+ launch_mcp_server: true
external/mcp-servers/hf-translation-reviewer/requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ requests>=2.31.0
3
+ gradio>=5.0.0
4
+
5
+ # LLM providers (optional, choose what you use)
6
+ openai>=1.12.0
7
+ anthropic>=0.34.0
8
+ google-generativeai>=0.5.0
9
+
10
+ # Typing helpers (optional, for static analysis)
11
+ typing-extensions>=4.8.0
12
+
13
+ # Python version note
14
+ # Python >=3.9 recommended
external/mcp-servers/hf-translation-reviewer/services.py ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import textwrap
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional, Tuple
8
+
9
+ from urllib.parse import urlparse
10
+
11
+ import requests
12
+
13
+ from setting import SETTINGS
14
+ from adapters import github_request, fetch_file_from_pr, dispatch_review
15
+
16
+
17
+ PROMPT_TEMPLATE = textwrap.dedent(
18
+ """
19
+ You are a meticulous bilingual reviewer checking a translation PR.
20
+
21
+ PR number: {pr_number}
22
+ PR URL: {pr_url}
23
+
24
+ Review the translated text against the original and focus on:
25
+ 1. Are there any typos or spelling mistakes?
26
+ 2. Are any sentences difficult to understand?
27
+ 3. Is the overall content hard to comprehend?
28
+
29
+ Always respond with strict JSON using this schema:
30
+ {{
31
+ "verdict": "request_changes" | "comment" | "approve",
32
+ "summary": "<High-level Markdown summary of the review findings>",
33
+ "comments": [
34
+ {{
35
+ "line": <1-based line number in the translated file>,
36
+ "issue": "<Short Markdown description of the problem>",
37
+ "suggested_edit": "<Replacement text for the entire translated line>",
38
+ "context": "<Exact current text of that line for grounding>"
39
+ }},
40
+ ...
41
+ ]
42
+ }}
43
+
44
+ Guidelines:
45
+ - Only include comments for issues that warrant direct feedback.
46
+ - When a concrete rewrite is possible, populate "suggested_edit" with the full replacement line exactly as it should appear after fixing the issue.
47
+ - Keep edits scoped to the referenced line; do not span multiple lines.
48
+ - Always copy the current text of that line verbatim into "context".
49
+ - Omit the "suggested_edit" field or set it to an empty string if no suggestion is available.
50
+ - Use "request_changes" when the identified problems must be fixed before merging.
51
+ - Use "approve" only when the translation is correct and clear with no changes needed.
52
+ - For optional improvements or general observations, use "comment".
53
+ - Keep suggestions tightly scoped so they can be applied as GitHub suggestions.
54
+ - Do not output partial fragments in "suggested_edit"; always provide the entire replacement line including unchanged portions.
55
+ - Use the line numbers from the "TRANSLATED TEXT WITH LINE NUMBERS" section.
56
+ """
57
+ ).strip()
58
+
59
+
60
+ # --------------------- Core helpers ------------------
61
+
62
+
63
+ def parse_pr_url(pr_url: str) -> Tuple[str, int]:
64
+ """Extract repo (owner/name) and PR number from a GitHub PR URL."""
65
+ if not pr_url:
66
+ raise ValueError("PR URL is required")
67
+ parsed = urlparse(pr_url)
68
+ parts = [p for p in parsed.path.split("/") if p]
69
+ # Expect: [owner, repo, 'pull', pr_number, ...]
70
+ if len(parts) < 4 or parts[2] != "pull":
71
+ raise ValueError(f"Not a valid GitHub PR URL: {pr_url}")
72
+ owner, repo, _, num = parts[0], parts[1], parts[2], parts[3]
73
+ if not num.isdigit():
74
+ raise ValueError(f"PR number not found in URL: {pr_url}")
75
+ return f"{owner}/{repo}", int(num)
76
+
77
+
78
+ def add_line_numbers(text: str) -> str:
79
+ return "\n".join(f"{i:04d}: {line}" for i, line in enumerate(text.splitlines(), 1))
80
+
81
+
82
+ def load_pr_files(
83
+ github_token: str,
84
+ pr_url: str,
85
+ original_path: str,
86
+ translated_path: str,
87
+ ) -> Tuple[str, int, str, str]:
88
+ repo_name, pr_number = parse_pr_url(pr_url)
89
+ pr_api = f"{SETTINGS.github_api_base}/repos/{repo_name}/pulls/{pr_number}"
90
+ pr_data = github_request(pr_api, github_token)
91
+ head_sha = pr_data.get("head", {}).get("sha")
92
+ if not head_sha:
93
+ raise RuntimeError(f"Unable to determine head SHA for PR {pr_number} in {repo_name}.")
94
+ original = fetch_file_from_pr(repo_name, pr_number, original_path, head_sha, github_token)
95
+ translated = fetch_file_from_pr(repo_name, pr_number, translated_path, head_sha, github_token)
96
+ return repo_name, pr_number, original, translated
97
+
98
+
99
+ def build_messages(
100
+ original: str,
101
+ translated: str,
102
+ pr_number: int,
103
+ pr_url: str,
104
+ ) -> Tuple[str, str]:
105
+ system_prompt = (
106
+ "You are an expert translation reviewer ensuring clarity, accuracy, "
107
+ "and readability of localized documentation."
108
+ )
109
+ user_prompt = (
110
+ f"{PROMPT_TEMPLATE}\n\n"
111
+ "----- ORIGINAL TEXT -----\n"
112
+ f"{original}\n\n"
113
+ "----- TRANSLATED TEXT -----\n"
114
+ f"{translated}\n\n"
115
+ "----- TRANSLATED TEXT WITH LINE NUMBERS -----\n"
116
+ f"{add_line_numbers(translated)}"
117
+ )
118
+ return system_prompt, user_prompt
119
+
120
+
121
+ def normalize_summary_for_body(summary: str) -> str:
122
+ """
123
+ GitHub review body로 쓸 텍스트 정리.
124
+ """
125
+ s = (summary or "").strip()
126
+ if not s:
127
+ return "LLM translation review"
128
+
129
+ if s.startswith("{") or s.startswith("["):
130
+ try:
131
+ obj = json.loads(s)
132
+ if isinstance(obj, dict):
133
+ inner = obj.get("summary")
134
+ if isinstance(inner, str) and inner.strip():
135
+ return inner.strip()
136
+ except Exception:
137
+ return s
138
+
139
+ return s
140
+
141
+
142
+ # ----------------------- Parsing & GitHub glue ----------------------
143
+
144
+
145
+ def _extract_json_candidates(raw_response: str) -> List[str]:
146
+ candidates: List[str] = []
147
+ for match in re.finditer(r"```(?:json)?\s*(\{.*?\})\s*```", raw_response, re.DOTALL):
148
+ snippet = match.group(1).strip()
149
+ if snippet:
150
+ candidates.append(snippet)
151
+ stripped = raw_response.strip()
152
+ if stripped:
153
+ candidates.append(stripped)
154
+ return candidates
155
+
156
+
157
+ def parse_review_response(raw_response: str) -> Tuple[str, str, List[Dict[str, object]]]:
158
+ parsed: Optional[Dict[str, object]] = None
159
+ for candidate in _extract_json_candidates(raw_response):
160
+ try:
161
+ parsed_candidate = json.loads(candidate)
162
+ except json.JSONDecodeError:
163
+ continue
164
+ if isinstance(parsed_candidate, dict):
165
+ parsed = parsed_candidate
166
+ break
167
+ if parsed is None:
168
+ return "comment", raw_response.strip(), []
169
+
170
+ verdict = parsed.get("verdict", "comment")
171
+ summary = parsed.get("summary", "").strip()
172
+ comments = parsed.get("comments", [])
173
+
174
+ if not isinstance(verdict, str):
175
+ verdict = "comment"
176
+ verdict = verdict.lower()
177
+ if verdict not in {"request_changes", "comment", "approve"}:
178
+ verdict = "comment"
179
+
180
+ if not summary:
181
+ summary = raw_response.strip()
182
+
183
+ if not isinstance(comments, list):
184
+ comments = []
185
+
186
+ normalized_comments: List[Dict[str, object]] = []
187
+ for comment in comments:
188
+ if not isinstance(comment, dict):
189
+ continue
190
+ line = comment.get("line")
191
+ issue = comment.get("issue", "").strip()
192
+ suggested_edit = comment.get("suggested_edit", "").strip()
193
+ context = comment.get("context", "").strip()
194
+ if not isinstance(line, int) or line <= 0:
195
+ continue
196
+ if not issue:
197
+ continue
198
+ normalized_comments.append(
199
+ {
200
+ "line": line,
201
+ "issue": issue,
202
+ "suggested_edit": suggested_edit,
203
+ "context": context,
204
+ }
205
+ )
206
+ return verdict, summary, normalized_comments
207
+
208
+
209
+ def review_event_from_verdict(verdict: str) -> str:
210
+ return {
211
+ "request_changes": "REQUEST_CHANGES",
212
+ "comment": "COMMENT",
213
+ "approve": "APPROVE",
214
+ }.get(verdict, "COMMENT")
215
+
216
+
217
+ def build_review_comments(
218
+ translated_path: str,
219
+ comments: List[Dict[str, object]],
220
+ ) -> List[Dict[str, object]]:
221
+ review_comments: List[Dict[str, object]] = []
222
+ for comment in comments:
223
+ line = int(comment["line"])
224
+ issue = str(comment["issue"]).strip()
225
+ raw_suggested = comment.get("suggested_edit", "")
226
+ if isinstance(raw_suggested, str):
227
+ suggested_edit = raw_suggested.rstrip("\r\n")
228
+ else:
229
+ suggested_edit = str(raw_suggested).rstrip("\r\n") if raw_suggested else ""
230
+ context = str(comment.get("context", "")).rstrip("\n")
231
+ full_line_suggestion = suggested_edit.rstrip("\n") if suggested_edit else ""
232
+
233
+ body_parts = [issue]
234
+ if context:
235
+ body_parts.append(f"> _Current text_: {context}")
236
+ if full_line_suggestion:
237
+ body_parts.append("```suggestion\n" + full_line_suggestion + "\n```")
238
+
239
+ body = "\n\n".join(body_parts).strip()
240
+ review_comments.append(
241
+ {
242
+ "path": translated_path,
243
+ "side": "RIGHT",
244
+ "line": line,
245
+ "body": body,
246
+ }
247
+ )
248
+ return review_comments
249
+
250
+
251
+ def attach_translated_line_context(
252
+ translated_text: str,
253
+ comments: List[Dict[str, object]],
254
+ ) -> None:
255
+ if not comments:
256
+ return
257
+ lines = translated_text.splitlines()
258
+ for comment in comments:
259
+ line_idx = comment.get("line")
260
+ if not isinstance(line_idx, int):
261
+ continue
262
+ list_index = line_idx - 1
263
+ if list_index < 0 or list_index >= len(lines):
264
+ continue
265
+ current_line = lines[list_index].rstrip("\n")
266
+ if not comment.get("context"):
267
+ comment["context"] = current_line
268
+
269
+
270
+ def build_github_review_payload(
271
+ body: str,
272
+ event: str = "COMMENT",
273
+ comments: Optional[List[Dict[str, object]]] = None,
274
+ ) -> Dict[str, object]:
275
+ payload: Dict[str, object] = {"event": event, "body": body}
276
+ if comments:
277
+ payload["comments"] = comments
278
+ return payload
279
+
280
+
281
+ def submit_pr_review(
282
+ repo_name: str,
283
+ pr_number: int,
284
+ github_token: str,
285
+ body: str,
286
+ event: str,
287
+ comments: Optional[List[Dict[str, object]]] = None,
288
+ allow_self_request_changes: bool = True,
289
+ ) -> Tuple[Dict, str]:
290
+ """
291
+ GitHub PR 리뷰 전송 (self-review REQUEST_CHANGES 우회 포함).
292
+ """
293
+ url = f"{SETTINGS.github_api_base}/repos/{repo_name}/pulls/{pr_number}/reviews"
294
+ headers = {
295
+ "Accept": "application/vnd.github.v3+json",
296
+ "Authorization": f"token {github_token}",
297
+ }
298
+
299
+ def _post(event_to_use: str, body_to_use: str) -> requests.Response:
300
+ payload = build_github_review_payload(
301
+ body=body_to_use,
302
+ event=event_to_use,
303
+ comments=comments,
304
+ )
305
+ return requests.post(url, headers=headers, json=payload, timeout=30)
306
+
307
+ # 1차 요청
308
+ response = _post(event, body)
309
+
310
+ if response.status_code == 401:
311
+ raise PermissionError(
312
+ "GitHub token is invalid or lacks permission to submit a review."
313
+ )
314
+
315
+ # 본인 PR + REQUEST_CHANGES 케이스 처리
316
+ if response.status_code == 422 and event == "REQUEST_CHANGES":
317
+ try:
318
+ error_payload = response.json()
319
+ except ValueError:
320
+ error_payload = {"message": response.text}
321
+ message = str(error_payload.get("message", ""))
322
+ errors = " ".join(str(item) for item in error_payload.get("errors", []))
323
+ combined_error = f"{message} {errors}".strip()
324
+
325
+ if "own pull request" in combined_error.lower():
326
+ if not allow_self_request_changes:
327
+ raise RuntimeError(
328
+ "GitHub does not allow REQUEST_CHANGES on your own pull request: "
329
+ + combined_error
330
+ )
331
+
332
+ fallback_event = "COMMENT"
333
+ fallback_body = "[REQUEST_CHANGES (self-review)]\n\n" + (body or "").strip()
334
+
335
+ comment_response = _post(fallback_event, fallback_body)
336
+ if comment_response.status_code >= 400:
337
+ raise RuntimeError(
338
+ "Failed to submit fallback self-review comment: "
339
+ f"HTTP {comment_response.status_code} - {comment_response.text}"
340
+ )
341
+ return comment_response.json(), "REQUEST_CHANGES_SELF"
342
+
343
+ if response.status_code >= 400:
344
+ raise RuntimeError(
345
+ "Failed to submit review: "
346
+ f"HTTP {response.status_code} - {response.text}"
347
+ )
348
+
349
+ return response.json(), event
350
+
351
+
352
+ # --------------------- High-level domain services ------------------
353
+
354
+
355
+ def prepare_translation_context(
356
+ github_token: str,
357
+ pr_url: str,
358
+ original_path: str,
359
+ translated_path: str,
360
+ ) -> Dict[str, object]:
361
+ """
362
+ PR에서 파일을 가져와 system/user prompt까지 구성.
363
+ """
364
+ repo_name, pr_number, original, translated = load_pr_files(
365
+ github_token=github_token,
366
+ pr_url=pr_url,
367
+ original_path=original_path,
368
+ translated_path=translated_path,
369
+ )
370
+ system_prompt, user_prompt = build_messages(
371
+ original=original,
372
+ translated=translated,
373
+ pr_number=pr_number,
374
+ pr_url=pr_url,
375
+ )
376
+ return {
377
+ "repo": repo_name,
378
+ "pr_number": pr_number,
379
+ "original": original,
380
+ "translated": translated,
381
+ "system_prompt": system_prompt,
382
+ "user_prompt": user_prompt,
383
+ }
384
+
385
+
386
+ def review_and_emit_payload(
387
+ provider: str,
388
+ provider_token: str,
389
+ model_name: str,
390
+ pr_url: str,
391
+ translated_path: str,
392
+ original: str,
393
+ translated: str,
394
+ ) -> Dict[str, object]:
395
+ """
396
+ LLM 리뷰 수행 후 verdict / summary / comments 및 GitHub payload 생성.
397
+ """
398
+ _, pr_number = parse_pr_url(pr_url)
399
+ system_prompt, user_prompt = build_messages(
400
+ original=original,
401
+ translated=translated,
402
+ pr_number=pr_number,
403
+ pr_url=pr_url,
404
+ )
405
+
406
+ raw = dispatch_review(
407
+ provider=provider,
408
+ token=provider_token,
409
+ system_prompt=system_prompt,
410
+ user_prompt=user_prompt,
411
+ model_name=model_name,
412
+ )
413
+ verdict, summary, comments = parse_review_response(raw)
414
+ attach_translated_line_context(translated, comments)
415
+
416
+ event = review_event_from_verdict(verdict)
417
+ github_comments = build_review_comments(translated_path, comments)
418
+ payload = build_github_review_payload(
419
+ body=summary,
420
+ event=event,
421
+ comments=github_comments,
422
+ )
423
+
424
+ return {
425
+ "verdict": verdict,
426
+ "summary": summary,
427
+ "comments": comments,
428
+ "event": event,
429
+ "payload": payload,
430
+ }
431
+
432
+
433
+ def submit_review_to_github(
434
+ github_token: str,
435
+ pr_url: str,
436
+ translated_path: str,
437
+ payload_or_review: Dict[str, object],
438
+ allow_self_request_changes: bool = True,
439
+ ) -> Dict[str, object]:
440
+ """
441
+ payload JSON 또는 review JSON을 입력받아 GitHub 리뷰 제출.
442
+ """
443
+ repo, pr_number = parse_pr_url(pr_url)
444
+
445
+ event = payload_or_review.get("event")
446
+ body = payload_or_review.get("body")
447
+ comments_obj = payload_or_review.get("comments")
448
+
449
+ comments: Optional[List[Dict[str, object]]] = None
450
+
451
+ if isinstance(event, str) and body:
452
+ # 이미 GitHub payload 형식
453
+ event_str = event
454
+ if isinstance(comments_obj, list):
455
+ comments = comments_obj
456
+ body_str = str(body)
457
+ else:
458
+ # review 형식 (verdict/summary/comments)
459
+ verdict = str(payload_or_review.get("verdict", "comment")).lower()
460
+ summary = str(payload_or_review.get("summary", "")).strip()
461
+ review_comments = payload_or_review.get("comments", [])
462
+ if not isinstance(review_comments, list):
463
+ review_comments = []
464
+
465
+ event_str = review_event_from_verdict(verdict)
466
+ body_str = summary if summary else "LLM translation review"
467
+ comments = build_review_comments(translated_path, review_comments)
468
+
469
+ if event_str == "REQUEST_CHANGES" and not body_str.strip() and not comments:
470
+ raise ValueError(
471
+ "REQUEST_CHANGES를 보내려면 review 본문 또는 코멘트가 하나 이상 필요합니다."
472
+ )
473
+
474
+ response, final_event = submit_pr_review(
475
+ repo_name=repo,
476
+ pr_number=pr_number,
477
+ github_token=github_token,
478
+ body=body_str,
479
+ event=event_str,
480
+ comments=comments,
481
+ allow_self_request_changes=allow_self_request_changes,
482
+ )
483
+ return {
484
+ "final_event": final_event,
485
+ "response": response,
486
+ }
487
+
488
+
489
+ def run_end_to_end(
490
+ provider: str,
491
+ provider_token: str,
492
+ model_name: str,
493
+ github_token: str,
494
+ pr_url: str,
495
+ original_path: str,
496
+ translated_path: str,
497
+ save_review: bool = False,
498
+ save_path: str = "review.json",
499
+ submit_review_flag: bool = False,
500
+ ) -> Dict[str, object]:
501
+ repo, pr_number, original, translated = load_pr_files(
502
+ github_token=github_token,
503
+ pr_url=pr_url,
504
+ original_path=original_path,
505
+ translated_path=translated_path,
506
+ )
507
+
508
+ system_prompt, user_prompt = build_messages(
509
+ original=original,
510
+ translated=translated,
511
+ pr_number=pr_number,
512
+ pr_url=pr_url,
513
+ )
514
+
515
+ raw = dispatch_review(
516
+ provider=provider,
517
+ token=provider_token,
518
+ system_prompt=system_prompt,
519
+ user_prompt=user_prompt,
520
+ model_name=model_name,
521
+ )
522
+
523
+ verdict, summary, comments = parse_review_response(raw)
524
+ attach_translated_line_context(translated, comments)
525
+
526
+ body_for_github = normalize_summary_for_body(summary)
527
+
528
+ github_comments = build_review_comments(translated_path, comments)
529
+ event = review_event_from_verdict(verdict)
530
+ payload = build_github_review_payload(
531
+ body=body_for_github,
532
+ event=event,
533
+ comments=github_comments,
534
+ )
535
+
536
+ saved_file_path: Optional[str] = None
537
+ if save_review:
538
+ p = Path(save_path).expanduser()
539
+ p.write_text(
540
+ json.dumps(
541
+ {
542
+ "verdict": verdict,
543
+ "summary": summary,
544
+ "comments": comments,
545
+ },
546
+ ensure_ascii=False,
547
+ indent=2,
548
+ ),
549
+ encoding="utf-8",
550
+ )
551
+ saved_file_path = str(p)
552
+
553
+ submission = None
554
+ if submit_review_flag:
555
+ resp, final_event = submit_pr_review(
556
+ repo_name=repo,
557
+ pr_number=pr_number,
558
+ github_token=github_token,
559
+ body=body_for_github,
560
+ event=event,
561
+ comments=github_comments,
562
+ allow_self_request_changes=True,
563
+ )
564
+ submission = {"final_event": final_event, "response": resp}
565
+
566
+ return {
567
+ "repo": repo,
568
+ "pr_number": pr_number,
569
+ "verdict": verdict,
570
+ "summary": summary,
571
+ "comments": comments,
572
+ "payload": payload,
573
+ "saved_file": saved_file_path,
574
+ "submission": submission,
575
+ }
external/mcp-servers/hf-translation-reviewer/setting.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional
6
+
7
+ import os
8
+
9
+ try:
10
+ import yaml # type: ignore
11
+ except Exception:
12
+ yaml = None
13
+
14
+
15
+ @dataclass
16
+ class AppSettings:
17
+ default_provider: str = "openai"
18
+ default_model: str = "gpt-5"
19
+ github_api_base: str = "https://api.github.com"
20
+ ui_title: str = "LLM Translation Reviewer (PR) — MCP Tools"
21
+ ui_share: bool = True
22
+ ui_launch_mcp_server: bool = True
23
+
24
+
25
+ def _load_yaml(path: Path) -> Dict[str, Any]:
26
+ if not path.is_file():
27
+ return {}
28
+ if yaml is None:
29
+ # yaml 없으면 config 없이 동작
30
+ return {}
31
+ with path.open("r", encoding="utf-8") as f:
32
+ data = yaml.safe_load(f) or {}
33
+ if not isinstance(data, dict):
34
+ return {}
35
+ return data
36
+
37
+
38
+ def load_settings(config_path: str = "configs/default.yaml") -> AppSettings:
39
+ cfg_path = Path(config_path)
40
+ data = _load_yaml(cfg_path)
41
+
42
+ provider_cfg = data.get("provider", {}) if isinstance(data.get("provider"), dict) else {}
43
+ github_cfg = data.get("github", {}) if isinstance(data.get("github"), dict) else {}
44
+ ui_cfg = data.get("ui", {}) if isinstance(data.get("ui"), dict) else {}
45
+
46
+ default_provider = os.getenv("DEFAULT_PROVIDER", provider_cfg.get("default", "openai"))
47
+ default_model = os.getenv("DEFAULT_MODEL", provider_cfg.get("model", "gpt-5"))
48
+ github_api_base = os.getenv("GITHUB_API_BASE", github_cfg.get("api_base", "https://api.github.com"))
49
+ ui_title = ui_cfg.get("title", "LLM Translation Reviewer (PR) — MCP Tools")
50
+ ui_share = bool(ui_cfg.get("share", True))
51
+ ui_launch_mcp_server = bool(ui_cfg.get("launch_mcp_server", True))
52
+
53
+ return AppSettings(
54
+ default_provider=default_provider,
55
+ default_model=default_model,
56
+ github_api_base=github_api_base,
57
+ ui_title=ui_title,
58
+ ui_share=ui_share,
59
+ ui_launch_mcp_server=ui_launch_mcp_server,
60
+ )
61
+
62
+
63
+ # 전역 설정 인스턴스
64
+ SETTINGS: AppSettings = load_settings()
external/mcp-servers/hf-translation-reviewer/tools.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict
4
+
5
+ from services import (
6
+ prepare_translation_context,
7
+ review_and_emit_payload,
8
+ submit_review_to_github,
9
+ run_end_to_end,
10
+ )
11
+
12
+
13
+ def tool_prepare(
14
+ github_token: str,
15
+ pr_url: str,
16
+ original_path: str,
17
+ translated_path: str,
18
+ ) -> Dict[str, object]:
19
+ """
20
+ Tool 1: Fetch Files + Build Prompts
21
+ """
22
+ return prepare_translation_context(
23
+ github_token=github_token,
24
+ pr_url=pr_url,
25
+ original_path=original_path,
26
+ translated_path=translated_path,
27
+ )
28
+
29
+
30
+ def tool_review_and_emit(
31
+ provider: str,
32
+ provider_token: str,
33
+ model_name: str,
34
+ pr_url: str,
35
+ translated_path: str,
36
+ original: str,
37
+ translated: str,
38
+ ) -> Dict[str, object]:
39
+ """
40
+ Tool 2: LLM Review + Emit Payload
41
+ """
42
+ return review_and_emit_payload(
43
+ provider=provider,
44
+ provider_token=provider_token,
45
+ model_name=model_name,
46
+ pr_url=pr_url,
47
+ translated_path=translated_path,
48
+ original=original,
49
+ translated=translated,
50
+ )
51
+
52
+
53
+ def tool_submit_review(
54
+ github_token: str,
55
+ pr_url: str,
56
+ translated_path: str,
57
+ payload_or_review: Dict[str, object],
58
+ allow_self_request_changes: bool = True,
59
+ ) -> Dict[str, object]:
60
+ """
61
+ Tool 3: Submit Review
62
+ """
63
+ return submit_review_to_github(
64
+ github_token=github_token,
65
+ pr_url=pr_url,
66
+ translated_path=translated_path,
67
+ payload_or_review=payload_or_review,
68
+ allow_self_request_changes=allow_self_request_changes,
69
+ )
70
+
71
+
72
+ def tool_end_to_end(
73
+ provider: str,
74
+ provider_token: str,
75
+ model_name: str,
76
+ github_token: str,
77
+ pr_url: str,
78
+ original_path: str,
79
+ translated_path: str,
80
+ save_review: bool = False,
81
+ save_path: str = "review.json",
82
+ submit_review_flag: bool = False,
83
+ ) -> Dict[str, object]:
84
+ """
85
+ Tool 4: End-to-End
86
+ """
87
+ return run_end_to_end(
88
+ provider=provider,
89
+ provider_token=provider_token,
90
+ model_name=model_name,
91
+ github_token=github_token,
92
+ pr_url=pr_url,
93
+ original_path=original_path,
94
+ translated_path=translated_path,
95
+ save_review=save_review,
96
+ save_path=save_path,
97
+ submit_review_flag=submit_review_flag,
98
+ )