evalstate HF Staff commited on
Commit
70c959f
·
verified ·
1 Parent(s): 961b4d3

Deploy OpenClaw PR API

Browse files
src/slop_farmer/app/pr_search.py CHANGED
@@ -11,6 +11,7 @@ get_pr_search_similar = pr_search_service.get_pr_search_similar
11
  get_pr_search_similar_lookup = pr_search_service.get_pr_search_similar_lookup
12
  get_pr_search_candidate_clusters = pr_search_service.get_pr_search_candidate_clusters
13
  get_pr_search_clusters = pr_search_service.get_pr_search_clusters
 
14
  get_pr_search_cluster = pr_search_service.get_pr_search_cluster
15
  explain_pr_search_pair = pr_search_service.explain_pr_search_pair
16
  probe_pr_search_live = pr_search_service.probe_pr_search_live
@@ -151,6 +152,29 @@ def format_pr_search_cluster(result: Mapping[str, Any]) -> str:
151
  return "\n".join(lines)
152
 
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  def format_pr_search_pair(result: Mapping[str, Any]) -> str:
155
  pair = result["pair"]
156
  lines = [
 
11
  get_pr_search_similar_lookup = pr_search_service.get_pr_search_similar_lookup
12
  get_pr_search_candidate_clusters = pr_search_service.get_pr_search_candidate_clusters
13
  get_pr_search_clusters = pr_search_service.get_pr_search_clusters
14
+ list_pr_search_clusters = pr_search_service.list_pr_search_clusters
15
  get_pr_search_cluster = pr_search_service.get_pr_search_cluster
16
  explain_pr_search_pair = pr_search_service.explain_pr_search_pair
17
  probe_pr_search_live = pr_search_service.probe_pr_search_live
 
152
  return "\n".join(lines)
153
 
154
 
155
+ def format_pr_search_cluster_list(result: Mapping[str, Any]) -> str:
156
+ lines = [
157
+ f"Repo: {result['repo']}",
158
+ f"Active snapshot: {result['snapshot_id']}",
159
+ "",
160
+ "Clusters:",
161
+ ]
162
+ clusters = result.get("clusters") or []
163
+ if not clusters:
164
+ lines.append("- none")
165
+ return "\n".join(lines)
166
+ for index, cluster in enumerate(clusters, start=1):
167
+ lines.append(
168
+ f"{index}. {cluster['cluster_id']} representative=PR #{cluster['representative_pr_number']} "
169
+ f"size={cluster['cluster_size']} avg={cluster['average_similarity']:.2f}"
170
+ )
171
+ if cluster.get("representative_title"):
172
+ lines.append(f" {cluster['representative_title']}")
173
+ if cluster.get("summary"):
174
+ lines.append(f" {cluster['summary']}")
175
+ return "\n".join(lines)
176
+
177
+
178
  def format_pr_search_pair(result: Mapping[str, Any]) -> str:
179
  pair = result["pair"]
180
  lines = [
src/slop_farmer/app/pr_search_api.py CHANGED
@@ -16,6 +16,7 @@ from slop_farmer.reports.pr_search_service import (
16
  get_pr_search_clusters,
17
  get_pr_search_similar_lookup,
18
  get_pr_search_status,
 
19
  run_pr_search_refresh,
20
  )
21
 
@@ -40,6 +41,8 @@ class PrSearchApiSettings:
40
  similar_limit_max: int = 50
41
  candidate_limit_default: int = 5
42
  candidate_limit_max: int = 20
 
 
43
  probe_limit_default: int = 10
44
  probe_limit_max: int = 25
45
 
@@ -70,6 +73,8 @@ class PrSearchApiSettings:
70
  similar_limit_max=_env_int("SIMILAR_LIMIT_MAX", 50),
71
  candidate_limit_default=_env_int("CANDIDATE_LIMIT_DEFAULT", 5),
72
  candidate_limit_max=_env_int("CANDIDATE_LIMIT_MAX", 20),
 
 
73
  probe_limit_default=_env_int("PROBE_LIMIT_DEFAULT", 10),
74
  probe_limit_max=_env_int("PROBE_LIMIT_MAX", 25),
75
  )
@@ -180,6 +185,25 @@ def create_app(settings: PrSearchApiSettings | None = None) -> FastAPI:
180
  repo_slug = _repo_slug(settings, owner, repo)
181
  return get_pr_search_cluster(settings.index_path, repo=repo_slug, cluster_id=cluster_id)
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  return app
184
 
185
 
 
16
  get_pr_search_clusters,
17
  get_pr_search_similar_lookup,
18
  get_pr_search_status,
19
+ list_pr_search_clusters,
20
  run_pr_search_refresh,
21
  )
22
 
 
41
  similar_limit_max: int = 50
42
  candidate_limit_default: int = 5
43
  candidate_limit_max: int = 20
44
+ cluster_list_limit_default: int = 50
45
+ cluster_list_limit_max: int = 200
46
  probe_limit_default: int = 10
47
  probe_limit_max: int = 25
48
 
 
73
  similar_limit_max=_env_int("SIMILAR_LIMIT_MAX", 50),
74
  candidate_limit_default=_env_int("CANDIDATE_LIMIT_DEFAULT", 5),
75
  candidate_limit_max=_env_int("CANDIDATE_LIMIT_MAX", 20),
76
+ cluster_list_limit_default=_env_int("CLUSTER_LIST_LIMIT_DEFAULT", 50),
77
+ cluster_list_limit_max=_env_int("CLUSTER_LIST_LIMIT_MAX", 200),
78
  probe_limit_default=_env_int("PROBE_LIMIT_DEFAULT", 10),
79
  probe_limit_max=_env_int("PROBE_LIMIT_MAX", 25),
80
  )
 
185
  repo_slug = _repo_slug(settings, owner, repo)
186
  return get_pr_search_cluster(settings.index_path, repo=repo_slug, cluster_id=cluster_id)
187
 
188
+ @app.get("/v1/repos/{owner}/{repo}/clusters")
189
+ async def cluster_list(
190
+ owner: str,
191
+ repo: str,
192
+ request: Request,
193
+ limit: int | None = None,
194
+ ) -> dict[str, Any]:
195
+ settings = request.app.state.settings
196
+ repo_slug = _repo_slug(settings, owner, repo)
197
+ return list_pr_search_clusters(
198
+ settings.index_path,
199
+ repo=repo_slug,
200
+ limit=_limit(
201
+ limit,
202
+ default=settings.cluster_list_limit_default,
203
+ maximum=settings.cluster_list_limit_max,
204
+ ),
205
+ )
206
+
207
  return app
208
 
209
 
src/slop_farmer/app/pr_search_client.py CHANGED
@@ -11,6 +11,7 @@ from typing import Any
11
 
12
  from slop_farmer.app.pr_search import (
13
  format_pr_search_cluster,
 
14
  format_pr_search_clusters,
15
  format_pr_search_similar,
16
  format_pr_search_status,
@@ -27,9 +28,10 @@ def build_parser() -> argparse.ArgumentParser:
27
  epilog=(
28
  "Examples:\n"
29
  " pr-search repo status\n"
30
- " pr-search pr similar 67096\n"
31
- " pr-search pr clusters 67096\n"
32
- " pr-search --json pr similar 67096 --mode live\n"
 
33
  " pr-search cluster view pr-scope-123-4\n"
34
  " pr-search -R openclaw/openclaw repo status"
35
  ),
@@ -70,14 +72,7 @@ def build_parser() -> argparse.ArgumentParser:
70
  description="Show the active indexed snapshot and row counts.",
71
  )
72
 
73
- pr_parser = subparsers.add_parser(
74
- "pr",
75
- help="Pull request operations.",
76
- description="Pull request operations.",
77
- )
78
- pr_subparsers = pr_parser.add_subparsers(dest="pr_command", required=True, metavar="SUBCOMMAND")
79
-
80
- similar = pr_subparsers.add_parser(
81
  "similar",
82
  help="Show similar PRs.",
83
  description="Find similar pull requests for one PR number.",
@@ -91,7 +86,7 @@ def build_parser() -> argparse.ArgumentParser:
91
  help="Lookup mode. Defaults to auto.",
92
  )
93
 
94
- clusters = pr_subparsers.add_parser(
95
  "clusters",
96
  help="Show cluster context for a PR.",
97
  description="Show assigned and candidate clusters for one PR number.",
@@ -113,6 +108,12 @@ def build_parser() -> argparse.ArgumentParser:
113
  cluster_subparsers = cluster_parser.add_subparsers(
114
  dest="cluster_command", required=True, metavar="SUBCOMMAND"
115
  )
 
 
 
 
 
 
116
  cluster_view = cluster_subparsers.add_parser(
117
  "view",
118
  help="Inspect one cluster.",
@@ -162,6 +163,13 @@ class PrSearchApiClient:
162
  owner, name = _split_repo(repo)
163
  return self._get_json(f"/v1/repos/{owner}/{name}/clusters/{cluster_id}")
164
 
 
 
 
 
 
 
 
165
  def _get_json(
166
  self,
167
  path: str,
@@ -195,31 +203,36 @@ def main(argv: list[str] | None = None) -> None:
195
  _emit(result, args.json, format_pr_search_status)
196
  return
197
 
198
- if args.command == "pr":
199
- if args.pr_command == "similar":
200
- result = client.get_similar(
201
- args.repo,
202
- number=args.number,
203
- limit=args.limit,
204
- mode=args.mode,
205
- )
206
- _emit(result, args.json, format_pr_search_similar)
207
- return
208
- if args.pr_command == "clusters":
209
- result = client.get_clusters(
210
- args.repo,
211
- number=args.number,
212
- limit=args.limit,
213
- mode=args.mode,
214
- )
215
- _emit(result, args.json, format_pr_search_clusters)
216
- return
217
 
218
- if args.command == "cluster" and args.cluster_command == "view":
219
- result = client.get_cluster(args.repo, cluster_id=args.cluster_id)
220
- _emit(result, args.json, format_pr_search_cluster)
 
 
 
 
 
221
  return
222
 
 
 
 
 
 
 
 
 
 
 
223
  raise RuntimeError("unsupported command")
224
  except RuntimeError as exc:
225
  print(f"error: {exc}", file=sys.stderr)
 
11
 
12
  from slop_farmer.app.pr_search import (
13
  format_pr_search_cluster,
14
+ format_pr_search_cluster_list,
15
  format_pr_search_clusters,
16
  format_pr_search_similar,
17
  format_pr_search_status,
 
28
  epilog=(
29
  "Examples:\n"
30
  " pr-search repo status\n"
31
+ " pr-search similar 67096\n"
32
+ " pr-search clusters 67096\n"
33
+ " pr-search cluster list --limit 20\n"
34
+ " pr-search --json similar 67096 --mode live\n"
35
  " pr-search cluster view pr-scope-123-4\n"
36
  " pr-search -R openclaw/openclaw repo status"
37
  ),
 
72
  description="Show the active indexed snapshot and row counts.",
73
  )
74
 
75
+ similar = subparsers.add_parser(
 
 
 
 
 
 
 
76
  "similar",
77
  help="Show similar PRs.",
78
  description="Find similar pull requests for one PR number.",
 
86
  help="Lookup mode. Defaults to auto.",
87
  )
88
 
89
+ clusters = subparsers.add_parser(
90
  "clusters",
91
  help="Show cluster context for a PR.",
92
  description="Show assigned and candidate clusters for one PR number.",
 
108
  cluster_subparsers = cluster_parser.add_subparsers(
109
  dest="cluster_command", required=True, metavar="SUBCOMMAND"
110
  )
111
+ cluster_list = cluster_subparsers.add_parser(
112
+ "list",
113
+ help="List clusters.",
114
+ description="List clusters in the active snapshot.",
115
+ )
116
+ cluster_list.add_argument("--limit", type=int, default=None, help="Maximum rows to return.")
117
  cluster_view = cluster_subparsers.add_parser(
118
  "view",
119
  help="Inspect one cluster.",
 
163
  owner, name = _split_repo(repo)
164
  return self._get_json(f"/v1/repos/{owner}/{name}/clusters/{cluster_id}")
165
 
166
+ def list_clusters(self, repo: str, *, limit: int | None) -> dict[str, Any]:
167
+ owner, name = _split_repo(repo)
168
+ return self._get_json(
169
+ f"/v1/repos/{owner}/{name}/clusters",
170
+ params=None if limit is None else {"limit": limit},
171
+ )
172
+
173
  def _get_json(
174
  self,
175
  path: str,
 
203
  _emit(result, args.json, format_pr_search_status)
204
  return
205
 
206
+ if args.command == "similar":
207
+ result = client.get_similar(
208
+ args.repo,
209
+ number=args.number,
210
+ limit=args.limit,
211
+ mode=args.mode,
212
+ )
213
+ _emit(result, args.json, format_pr_search_similar)
214
+ return
 
 
 
 
 
 
 
 
 
 
215
 
216
+ if args.command == "clusters":
217
+ result = client.get_clusters(
218
+ args.repo,
219
+ number=args.number,
220
+ limit=args.limit,
221
+ mode=args.mode,
222
+ )
223
+ _emit(result, args.json, format_pr_search_clusters)
224
  return
225
 
226
+ if args.command == "cluster":
227
+ if args.cluster_command == "list":
228
+ result = client.list_clusters(args.repo, limit=args.limit)
229
+ _emit(result, args.json, format_pr_search_cluster_list)
230
+ return
231
+ if args.cluster_command == "view":
232
+ result = client.get_cluster(args.repo, cluster_id=args.cluster_id)
233
+ _emit(result, args.json, format_pr_search_cluster)
234
+ return
235
+
236
  raise RuntimeError("unsupported command")
237
  except RuntimeError as exc:
238
  print(f"error: {exc}", file=sys.stderr)
src/slop_farmer/reports/pr_search_service.py CHANGED
@@ -418,6 +418,44 @@ def get_pr_search_cluster(
418
  connection.close()
419
 
420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  def explain_pr_search_pair(
422
  db_path: Path,
423
  *,
 
418
  connection.close()
419
 
420
 
421
+ def list_pr_search_clusters(
422
+ db_path: Path,
423
+ *,
424
+ repo: str | None = None,
425
+ limit: int = 50,
426
+ ) -> dict[str, Any]:
427
+ connection = connect_pr_search_db(db_path, read_only=True)
428
+ try:
429
+ active_run = resolve_active_run(connection, repo=repo)
430
+ run_id = str(active_run["id"])
431
+ rows = fetch_rows(
432
+ connection,
433
+ """
434
+ SELECT
435
+ cl.*,
436
+ d.title AS representative_title,
437
+ d.html_url AS representative_html_url,
438
+ d.state AS representative_state,
439
+ d.draft AS representative_draft
440
+ FROM pr_scope_clusters AS cl
441
+ LEFT JOIN pr_search_documents AS d
442
+ ON d.run_id = cl.run_id AND d.pr_number = cl.representative_pr_number
443
+ WHERE cl.run_id = ?
444
+ ORDER BY cl.cluster_size DESC, cl.average_similarity DESC, cl.cluster_id
445
+ LIMIT ?
446
+ """,
447
+ [run_id, limit],
448
+ )
449
+ return {
450
+ "repo": active_run["repo"],
451
+ "snapshot_id": active_run["snapshot_id"],
452
+ "run_id": run_id,
453
+ "clusters": [_cluster_summary(row) for row in rows],
454
+ }
455
+ finally:
456
+ connection.close()
457
+
458
+
459
  def explain_pr_search_pair(
460
  db_path: Path,
461
  *,