Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 46 additions & 12 deletions lqh/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2186,29 +2186,63 @@ async def _handle_hf_push_permission(
elif "don't ask again for this repo" in response:
grant_hf_permission(self.project_dir, repo_id=repo_id)

# Execute the push
from lqh.tools.handlers import _execute_hf_push, _get_hf_api, _validate_path
# Execute the push.
#
# This mirrors the dispatch at the end of handlers.handle_hf_push. The
# two paths have to agree on repo type, and previously they did not:
# this one assumed dataset, globbed for parquet, and indexed
# parquet_files[0], which raises IndexError on a model folder.
from lqh.tools.handlers import (
_detect_hf_repo_type,
_execute_hf_push_dataset,
_execute_hf_push_model,
_get_hf_api,
_validate_path,
)

api = _get_hf_api(self.project_dir)
local_path = push_args.get("local_path", "")
target = _validate_path(self.project_dir, local_path)

# Find parquet file
if target.is_dir():
repo_type = push_args.get("repo_type")
detected, parquet_files, _model_files = _detect_hf_repo_type(target)
if repo_type is None:
repo_type = detected
if repo_type is None:
# handle_hf_push rejects this before ever prompting, so reaching it
# here means the folder changed between the prompt and the answer.
return ToolResult.fail(
"validation",
(
f"Error: '{local_path}' is no longer recognizable as a dataset "
f"or model folder. Re-run hf_push."
),
)

if repo_type == "dataset":
data_parquet = target / "data.parquet"
parquet_files = list(target.glob("*.parquet"))
parquet_path = data_parquet if data_parquet.exists() else parquet_files[0]
else:
parquet_path = target
parquet_path = (
data_parquet if data_parquet.exists() else target / parquet_files[0]
)
return await _execute_hf_push_dataset(
self.project_dir,
target,
parquet_path,
local_path,
repo_id,
push_args.get("private", True),
push_args.get("split", "train"),
push_args.get("subset"),
push_args.get("commit_message"),
api,
)

return await _execute_hf_push(
return await _execute_hf_push_model(
self.project_dir,
parquet_path,
target,
local_path,
repo_id,
push_args.get("private", True),
push_args.get("split", "train"),
push_args.get("subset"),
push_args.get("commit_message"),
api,
)
Expand Down
73 changes: 73 additions & 0 deletions tests/unit/test_tool_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import pytest

from lqh.tools.handlers import (
ToolResult,
_validate_path,
execute_tool,
handle_create_file,
Expand Down Expand Up @@ -637,3 +638,75 @@ def whoami(self):
assert result.content == "PERMISSION_REQUIRED"
# repo_id was auto-generated inside the handler; the key exposes it.
assert result.permission_key == f"hf_push:tester/{tmp_path.name}-demo"


class TestHfPushPermissionExecutes:
"""The push that runs *after* the user approves must reach the Hub.

`handle_hf_push` returns PERMISSION_REQUIRED on the first push to a repo,
so this path is the one every new user takes. It dispatches separately from
the already-approved path, and the two have to agree about repo type.
"""

@staticmethod
def _agent(project_dir):
from lqh.agent import Agent

agent = Agent.__new__(Agent)
agent.project_dir = project_dir
return agent

@pytest.mark.asyncio
async def test_approved_dataset_push_reaches_the_dataset_executor(
self, tmp_path: Path, monkeypatch
) -> None:
seen = {}

async def fake_dataset(project_dir, target, parquet_path, *a, **k):
seen["parquet"] = parquet_path
return ToolResult(content="pushed dataset")

monkeypatch.setattr("lqh.tools.handlers._get_hf_api", lambda *a, **k: object())
monkeypatch.setattr(
"lqh.tools.handlers._execute_hf_push_dataset", fake_dataset
)
dataset = tmp_path / "datasets" / "demo"
dataset.mkdir(parents=True)
(dataset / "data.parquet").write_bytes(b"x")

result = await self._agent(tmp_path)._handle_hf_push_permission(
"Push once, ask again next time",
{"local_path": "datasets/demo"},
permission_key="hf_push:tester/demo",
)
assert result.content == "pushed dataset"
assert seen["parquet"].name == "data.parquet"

@pytest.mark.asyncio
async def test_approved_model_push_reaches_the_model_executor(
self, tmp_path: Path, monkeypatch
) -> None:
"""A model folder has no parquet at all.

The previous implementation globbed for parquet unconditionally and
indexed [0], so this folder could not be pushed after approval.
"""
called = {}

async def fake_model(project_dir, target, local_path, *a, **k):
called["local_path"] = local_path
return ToolResult(content="pushed model")

monkeypatch.setattr("lqh.tools.handlers._get_hf_api", lambda *a, **k: object())
monkeypatch.setattr("lqh.tools.handlers._execute_hf_push_model", fake_model)
model = tmp_path / "checkpoints" / "lfm"
model.mkdir(parents=True)
(model / "config.json").write_text("{}")

result = await self._agent(tmp_path)._handle_hf_push_permission(
"Push once, ask again next time",
{"local_path": "checkpoints/lfm"},
permission_key="hf_push:tester/lfm",
)
assert result.content == "pushed model"
assert called["local_path"] == "checkpoints/lfm"