Skills: write side — whole-resource, grouping folder and single-file writes (/v2/skills)

Open
#137 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
45/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
python
Domain
api

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

enhancement
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@developmentopen_api_core.yaml is lossy here):

  • The whole-resource PUT is one multipart part per file. The spec advertises a single file binary part, but ComplexResourceController.put collects uploads into a map keyed by upload.filename(), and each part's filename is the file's relative path inside the skill. The .dial-resource marker is synthesized server-side, so a client can never write or corrupt it. _internal_types/_http_request.py's RequestFiles already allows Sequence[tuple[str, FileTypes]], so the transport supports this today.
  • Server-side validation (SkillHandler): SKILL.md must exist at the skill root and open with YAML frontmatter delimited by ---, carrying a non-empty name and description; version is optional and cached if present. Violations are 400. Single-file mutations re-run validation — editing SKILL.md re-parses the frontmatter, and deleting it is rejected outright.
  • Writes return an empty body with only an ETag response header. This is unlike files.upload, which returns a parsed FileItem, so these methods need a deliberate return type (see the open questions below).
  • If-Match semantics are unusual. Per Core's own parameter docs on the whole-resource PUT: 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 the If-None-Match: "*" convention files.upload uses, and it needs to be explicit in both the signature and the README.
  • Grouping-folder DELETE succeeds only if the folder is empty (409 otherwise); GET on a trailing-slash path answers 400 by 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:

  1. Return type for writes. These endpoints hand back only an ETag. Options: a small typed result model (e.g. SkillWriteResult(etag: str | None)), a plain str | None, or None as files.delete does. Returning the ETag matters because it is the input to the next write's If-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.
  2. 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) and download_to(url, local_dir) (extract the ZIP from #136's download) over stdlib zipfile + os.walk, or to keep the surface strictly byte-level and let callers do it.
  3. Client-side pre-validation. Whether to check the SKILL.md frontmatter contract locally before the request to give a better error than Core's 400. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from epam/ai-dial-client-python

All issues in epam/ai-dial-client-python

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.