Skills: write side — whole-resource, grouping folder and single-file writes (/v2/skills)
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 45/100
Research direction
Start with the Skills/AsyncSkills resource from #136 and compare the multipart and ETag patterns in files.py. Resolve the return type, local-folder helpers, and client-side validation questions before implementing the six operations. Extend tests/resources/skills/ for multipart naming, If-Match behavior, ETags, and validation errors, then update README.md and exports as required by CLAUDE.md.
Written by the indexing model from the issue text.
Description
Name and Version
aidial-client 0.16.1
What is the problem this feature will solve?
Child of #135.
DIAL Core's /v2/skills API (epam/ai-dial-core#1633) exposes six write operations, and the Python client can reach none of them. With only the read side (#136) a caller can consume skills that already exist but cannot create, update or delete one, cannot organise skills into grouping folders, and cannot edit a single file inside a skill — so there is no programmatic authoring path at all.
What is the feature you are proposing to solve the problem?
Extend the Skills / AsyncSkills resource added by #136 with the write half of Core's API.
| Method | Endpoint | Notes |
|---|---|---|
save / upload |
PUT /v2/skills/{bucket}/{path} |
multipart/form-data, replaces the whole skill |
delete |
DELETE /v2/skills/{bucket}/{path} |
tombstones the resource |
create_folder |
PUT /v2/skills/{bucket}/{path}/ |
grouping folder; 400 if it already exists |
delete_folder |
DELETE /v2/skills/{bucket}/{path}/ |
409 if not empty |
upload_file |
PUT /v2/skills/{bucket}/{path}/files/{filePath} |
one part; atomic within the skill |
delete_file |
DELETE /v2/skills/{bucket}/{path}/files/{filePath} |
rejects deleting SKILL.md |
Core behaviour that must drive the design (checked against epam/ai-dial-core@development — open_api_core.yaml is lossy here):
- The whole-resource
PUTis one multipart part per file. The spec advertises a singlefilebinary part, butComplexResourceController.putcollects uploads into a map keyed byupload.filename(), and each part's filename is the file's relative path inside the skill. The.dial-resourcemarker is synthesized server-side, so a client can never write or corrupt it._internal_types/_http_request.py'sRequestFilesalready allowsSequence[tuple[str, FileTypes]], so the transport supports this today. - Server-side validation (
SkillHandler):SKILL.mdmust exist at the skill root and open with YAML frontmatter delimited by---, carrying a non-emptynameanddescription;versionis optional and cached if present. Violations are400. Single-file mutations re-run validation — editingSKILL.mdre-parses the frontmatter, and deleting it is rejected outright. - Writes return an empty body with only an
ETagresponse header. This is unlikefiles.upload, which returns a parsedFileItem, so these methods need a deliberate return type (see the open questions below). If-Matchsemantics are unusual. Per Core's own parameter docs on the whole-resourcePUT: supply the current ETag to replace that version,*to overwrite whatever exists, or omit the header to create only if the resource does not already exist. That is the inverse of theIf-None-Match: "*"conventionfiles.uploaduses, and it needs to be explicit in both the signature and the README.- Grouping-folder
DELETEsucceeds only if the folder is empty (409otherwise);GETon a trailing-slash path answers400by design — use the metadata listing from #136. - Per-resource limits are configurable in Core (
maxFiles, default ~1000;maxTotalBytes, default ~1 GB) on top of the existing 512 MB per-file cap.
Open questions to settle in this issue before implementing:
- Return type for writes. These endpoints hand back only an
ETag. Options: a small typed result model (e.g.SkillWriteResult(etag: str | None)), a plainstr | None, orNoneasfiles.deletedoes. Returning the ETag matters because it is the input to the next write'sIf-Match, and — per #136 — the children metadata listing does not expose it, so a write response is one of the few places to obtain it. - Local-folder helpers. A skill is inherently a directory on disk. Whether to add
upload_folder(url, local_dir)(walk a directory into multipart parts) anddownload_to(url, local_dir)(extract the ZIP from #136'sdownload) over stdlibzipfile+os.walk, or to keep the surface strictly byte-level and let callers do it. - Client-side pre-validation. Whether to check the
SKILL.mdfrontmatter contract locally before the request to give a better error than Core's400. This would want a YAML parser, which the library does not currently depend on — probably not worth a new dependency, but worth recording the decision.
Tests extend tests/resources/skills/: multipart part-naming (assert one part per file, part filename = relative path), the If-Match create-vs-overwrite matrix, ETag surfaced on every write, 409 on deleting a non-empty grouping folder, and 400 when SKILL.md is missing or its frontmatter lacks name / description.
Per CLAUDE.md's PR checklist: README.md's Skills section gains sync + async write examples and sample responses, and any new types are exported.
What alternatives have you considered?
Accept a ZIP archive for the whole-resource PUT, mirroring the GET. Rejected — it does not match the endpoint. Core's contract is deliberately asymmetric: the GET streams a ZIP, but the PUT takes multipart/form-data with one part per file so the server can validate and synthesize the marker itself. Sending a ZIP would simply be rejected.
Let callers assemble the multipart body themselves and expose only a thin passthrough. Rejected. Part-naming is the single easiest thing to get wrong here — the part filename must be the relative path inside the skill, which is neither obvious nor documented correctly in open_api_core.yaml — so encoding it once in the library is most of the value.
Fold this into #136 and ship the whole API at once. Rejected. The reads have no open design questions; this issue has three. Bundling them would delay a complete, useful read capability behind an unrelated discussion.
Model the write path on prompts.py. Rejected. Prompts.save is json_data-shaped and returns a parsed PromptItem; every operation here is multipart- or ETag-header-shaped, so files.py is the closer template — as it is for #136.
- Dominant language
- Python
- Stars
- 7
- Forks
- 2
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 8
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from epam/ai-dial-client-python
-
enhancement
Difficulty 3/5 1-2 days Newbie friendliness 48/100
epam/ai-dial-client-python#140 ·
-
enhancement
Difficulty 5/5 Over a week Newbie friendliness 35/100
epam/ai-dial-client-python#135 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 38/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 25/100
All issues in epam/ai-dial-client-python
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100