Skip to content

Commit 9d9fab5

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
feat: Add Create Skill method for Vertex AI Skill Registry
PiperOrigin-RevId: 910161153
1 parent 9ea4aa6 commit 9d9fab5

8 files changed

Lines changed: 1572 additions & 0 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
"""Tests the skills.create() method against the Vertex AI endpoint using replays."""
16+
17+
import io
18+
import os
19+
import tempfile
20+
import zipfile
21+
22+
from tests.unit.vertexai.genai.replays import pytest_helper
23+
from vertexai._genai import types
24+
25+
# MANDATORY: Initialize the replay test framework for this module
26+
pytestmark = pytest_helper.setup(
27+
file=__file__,
28+
globals_for_file=globals(),
29+
)
30+
31+
32+
def test_create_skill(client):
33+
"""Tests the creation of a skill using `client.skills.create()`."""
34+
# Target the autopush sandbox endpoint for the Skill Registry API
35+
client._api_client._http_options.base_url = (
36+
"https://us-central1-autopush-aiplatform.sandbox.googleapis.com"
37+
)
38+
39+
with tempfile.TemporaryDirectory() as tmpdir:
40+
# Create a dummy skill structure (SKILL.md is required by the spec)
41+
with open(os.path.join(tmpdir, "SKILL.md"), "w") as f:
42+
f.write("# My Replay Skill\nThis is a test skill for replay tests.")
43+
44+
skill = client.skills.create(
45+
display_name="My Replay Skill",
46+
description="My Replay Skill Description",
47+
local_path=tmpdir,
48+
config=types.CreateSkillConfig(wait_for_completion=True),
49+
)
50+
51+
assert skill.name is not None
52+
assert skill.display_name == 'My Replay Skill'
53+
assert skill.description == 'My Replay Skill Description'
54+
55+
56+
def test_create_skill_with_prezipped_bytes(client):
57+
"""Tests the creation of a skill with pre-zipped bytes."""
58+
# Target the autopush sandbox endpoint for the Skill Registry API
59+
client._api_client._http_options.base_url = (
60+
'https://us-central1-autopush-aiplatform.sandbox.googleapis.com'
61+
)
62+
63+
zip_buffer = io.BytesIO()
64+
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
65+
zip_file.writestr('SKILL.md', '# My Zipped Replay Skill\nThis is a test.')
66+
zipped_bytes = zip_buffer.getvalue()
67+
68+
skill = client.skills.create(
69+
display_name='My Zipped Replay Skill',
70+
description='My Zipped Replay Skill Description',
71+
zipped_filesystem=zipped_bytes,
72+
config=types.CreateSkillConfig(wait_for_completion=True),
73+
)
74+
75+
assert skill.name is not None
76+
assert skill.display_name == 'My Zipped Replay Skill'
77+
assert skill.description == 'My Zipped Replay Skill Description'
78+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Tests the skills.get() method against the autopush endpoint."""
2+
3+
from google.api_core import exceptions
4+
from tests.unit.vertexai.genai.replays import pytest_helper
5+
import pytest
6+
7+
PROJECT_ID = "srbai-testing"
8+
REGION = "us-central1"
9+
# SKILL_ID = "5578834038405201920"
10+
SKILL_ID = "7184367305562783744"
11+
ENDPOINT = f"{REGION}-autopush-aiplatform.sandbox.googleapis.com"
12+
13+
# # Configure HTTP options to target the autopush endpoint
14+
# my_http_options = genai_types.HttpOptions(
15+
# api_version="v1beta1",
16+
# base_url=f"https://{ENDPOINT}/v1beta1/" # <---APPENDED /v1beta1/ here
17+
# )
18+
19+
pytestmark = pytest_helper.setup(
20+
file=__file__,
21+
globals_for_file=globals(),
22+
# http_options=my_http_options,
23+
)
24+
25+
26+
def test_get_skill(client): # client fixture is injected by pytest_helper.setup
27+
"""Tests the skills.get() method against the autopush endpoint."""
28+
29+
client._api_client._http_options.base_url = (
30+
"https://us-central1-autopush-aiplatform.sandbox.googleapis.com"
31+
)
32+
skill_name = f"projects/{PROJECT_ID}/locations/{REGION}/skills/{SKILL_ID}"
33+
34+
try:
35+
skill = client.skills.get(name=skill_name)
36+
assert skill.name == skill_name
37+
38+
except exceptions.GoogleAPIError as e:
39+
pytest.fail(f"Error calling client.skills.get(): {e}")
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# //third_party/py/google/cloud/aiplatform/tests/unit/vertexai/genai/test_genai_skills.py
16+
import json
17+
from unittest import mock
18+
19+
from vertexai import _genai as genai
20+
from vertexai._genai import client as vertexai_client
21+
from google.genai import types as genai_types
22+
import pytest
23+
24+
25+
@pytest.fixture
26+
def skills_client():
27+
creds = mock.MagicMock()
28+
creds.token = "test_token"
29+
client = vertexai_client.Client(
30+
project="test-project", location="test-location", credentials=creds
31+
)
32+
return client.skills
33+
34+
35+
@pytest.fixture
36+
def async_skills_client():
37+
creds = mock.MagicMock()
38+
creds.token = "test_token"
39+
client = vertexai_client.Client(
40+
project="test-project", location="test-location", credentials=creds
41+
)
42+
return client.aio.skills
43+
44+
45+
class TestGenaiSkills:
46+
mock_get_skill_response = {
47+
"name": "projects/test-project/locations/test-location/skills/test-skill",
48+
"displayName": "My Test Skill",
49+
}
50+
51+
def test_get_skill(self, skills_client):
52+
"""Tests the get_skill method."""
53+
with mock.patch.object(skills_client._api_client, "request") as request_mock:
54+
request_mock.return_value = genai_types.HttpResponse(
55+
body=json.dumps(self.mock_get_skill_response)
56+
)
57+
skill_name = (
58+
"projects/test-project/locations/test-location/skills/test-skill"
59+
)
60+
skill = skills_client.get(name=skill_name)
61+
request_mock.assert_called_with(
62+
"get",
63+
skill_name,
64+
{"_url": {"name": skill_name}},
65+
None,
66+
)
67+
assert isinstance(skill, genai.types.Skill)
68+
assert skill.name == skill_name
69+
assert skill.display_name == "My Test Skill"
70+
71+
def test_create_skill(self, skills_client):
72+
"""Tests the create_skill method with wait_for_completion=True."""
73+
import tempfile
74+
import os
75+
76+
with tempfile.TemporaryDirectory() as tmpdir:
77+
# Create a dummy file in tmpdir
78+
with open(os.path.join(tmpdir, "SKILL.md"), "w") as f:
79+
f.write("# Test Skill")
80+
81+
# Prepare mock responses
82+
pending_op = {
83+
"name": "projects/test-project/locations/test-location/skills/test-skill/operations/op-123",
84+
"done": False,
85+
}
86+
finished_op = {
87+
"name": "projects/test-project/locations/test-location/skills/test-skill/operations/op-123",
88+
"done": True,
89+
"response": {
90+
"name": "projects/test-project/locations/test-location/skills/test-skill",
91+
"displayName": "My Test Skill",
92+
"description": "My Test Skill Description",
93+
},
94+
}
95+
96+
# Final Skill response returned by get call
97+
skill_response = {
98+
"name": "projects/test-project/locations/test-location/skills/test-skill",
99+
"displayName": "My Test Skill",
100+
"description": "My Test Skill Description",
101+
}
102+
103+
with mock.patch.object(
104+
skills_client._api_client, "request"
105+
) as request_mock:
106+
request_mock.side_effect = [
107+
genai_types.HttpResponse(body=json.dumps(pending_op)),
108+
genai_types.HttpResponse(body=json.dumps(finished_op)),
109+
genai_types.HttpResponse(body=json.dumps(skill_response)),
110+
]
111+
112+
# We mock time.sleep to speed up the test
113+
with mock.patch("time.sleep", return_value=None):
114+
skill = skills_client.create(
115+
display_name="My Test Skill",
116+
description="My Test Skill Description",
117+
local_path=tmpdir,
118+
config={"wait_for_completion": True},
119+
)
120+
121+
# Assertions
122+
assert request_mock.call_count == 3
123+
124+
# Verify POST request
125+
post_call = request_mock.call_args_list[0]
126+
assert post_call[0][0] == "post"
127+
assert post_call[0][1] == "skills"
128+
129+
post_body = post_call[0][2]
130+
assert post_body["displayName"] == "My Test Skill"
131+
assert post_body["description"] == "My Test Skill Description"
132+
assert isinstance(post_body["zippedFilesystem"], str)
133+
134+
# Verify GET request (polling)
135+
get_call = request_mock.call_args_list[1]
136+
assert get_call[0][0] == "get"
137+
assert (
138+
get_call[0][1]
139+
== "projects/test-project/locations/test-location/skills/test-skill/operations/op-123"
140+
)
141+
142+
# Verify final GET request to fetch the skill
143+
get_skill_call = request_mock.call_args_list[2]
144+
assert get_skill_call[0][0] == "get"
145+
assert (
146+
get_skill_call[0][1]
147+
== "projects/test-project/locations/test-location/skills/test-skill"
148+
)
149+
150+
# Verify returned skill
151+
assert isinstance(skill, genai.types.Skill)
152+
assert (
153+
skill.name
154+
== "projects/test-project/locations/test-location/skills/test-skill"
155+
)
156+
assert skill.display_name == "My Test Skill"
157+
assert skill.description == "My Test Skill Description"
158+
159+
def test_create_skill_no_wait(self, skills_client):
160+
"""Tests the create_skill method with wait_for_completion=False."""
161+
import tempfile
162+
import os
163+
164+
with tempfile.TemporaryDirectory() as tmpdir:
165+
with open(os.path.join(tmpdir, "SKILL.md"), "w") as f:
166+
f.write("# Test Skill")
167+
168+
pending_op = {
169+
"name": "projects/test-project/locations/test-location/skills/test-skill/operations/op-123",
170+
"done": False,
171+
}
172+
173+
with mock.patch.object(
174+
skills_client._api_client, "request"
175+
) as request_mock:
176+
request_mock.return_value = genai_types.HttpResponse(
177+
body=json.dumps(pending_op)
178+
)
179+
180+
operation = skills_client.create(
181+
display_name="My Test Skill",
182+
description="My Test Skill Description",
183+
local_path=tmpdir,
184+
config={"wait_for_completion": False},
185+
)
186+
187+
# Assertions
188+
assert request_mock.call_count == 1
189+
assert isinstance(operation, genai.types.SkillOperation)
190+
assert (
191+
operation.name
192+
== "projects/test-project/locations/test-location/skills/test-skill/operations/op-123"
193+
)
194+
assert not operation.done
195+
196+
@pytest.mark.asyncio
197+
async def test_create_skill_async(self, async_skills_client):
198+
"""Tests the create_skill method asynchronously with wait_for_completion=True."""
199+
import tempfile
200+
import os
201+
202+
with tempfile.TemporaryDirectory() as tmpdir:
203+
with open(os.path.join(tmpdir, "SKILL.md"), "w") as f:
204+
f.write("# Test Skill")
205+
206+
pending_op = {
207+
"name": "projects/test-project/locations/test-location/skills/test-skill/operations/op-123",
208+
"done": False,
209+
}
210+
finished_op = {
211+
"name": "projects/test-project/locations/test-location/skills/test-skill/operations/op-123",
212+
"done": True,
213+
"response": {
214+
"name": "projects/test-project/locations/test-location/skills/test-skill",
215+
"displayName": "My Test Skill",
216+
"description": "My Test Skill Description",
217+
},
218+
}
219+
220+
# Final Skill response returned by async get call
221+
skill_response = {
222+
"name": "projects/test-project/locations/test-location/skills/test-skill",
223+
"displayName": "My Test Skill",
224+
"description": "My Test Skill Description",
225+
}
226+
227+
with mock.patch.object(
228+
async_skills_client._api_client, "async_request"
229+
) as request_mock:
230+
request_mock.side_effect = [
231+
genai_types.HttpResponse(body=json.dumps(pending_op)),
232+
genai_types.HttpResponse(body=json.dumps(finished_op)),
233+
genai_types.HttpResponse(body=json.dumps(skill_response)),
234+
]
235+
236+
with mock.patch("asyncio.sleep", new_callable=mock.AsyncMock):
237+
skill = await async_skills_client.create(
238+
display_name="My Test Skill",
239+
description="My Test Skill Description",
240+
local_path=tmpdir,
241+
config={"wait_for_completion": True},
242+
)
243+
244+
# Assertions
245+
assert request_mock.call_count == 3
246+
247+
# Verify POST request
248+
post_call = request_mock.call_args_list[0]
249+
assert post_call[0][0] == "post"
250+
assert post_call[0][1] == "skills"
251+
252+
# Verify final GET request to fetch the skill
253+
get_skill_call = request_mock.call_args_list[2]
254+
assert get_skill_call[0][0] == "get"
255+
assert (
256+
get_skill_call[0][1]
257+
== "projects/test-project/locations/test-location/skills/test-skill"
258+
)
259+
260+
# Verify returned skill
261+
assert isinstance(skill, genai.types.Skill)
262+
assert (
263+
skill.name
264+
== "projects/test-project/locations/test-location/skills/test-skill"
265+
)
266+
assert skill.display_name == "My Test Skill"
267+
assert skill.description == "My Test Skill Description"

0 commit comments

Comments
 (0)