Spaces:
Sleeping
Sleeping
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
|
| 31 |
-
" pr-search
|
| 32 |
-
" pr-search
|
|
|
|
| 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 |
-
|
| 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 =
|
| 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 == "
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 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 == "
|
| 219 |
-
result = client.
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
*,
|