You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Core behaviour that must drive the design (checked against epam/ai-dial-core@development — open_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.
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 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 Skills: read side — get_metadata, list_files, get_file, download (/v2/skills) #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) and download_to(url, local_dir) (extract the ZIP from Skills: read side — get_metadata, list_files, get_file, download (/v2/skills) #136's download) over stdlib zipfile + os.walk, or to keep the surface strictly byte-level and let callers do it.
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.
Name and Version
aidial-client 0.16.1
What is the problem this feature will solve?
Child of #135.
DIAL Core's
/v2/skillsAPI (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/AsyncSkillsresource added by #136 with the write half of Core's API.save/uploadPUT /v2/skills/{bucket}/{path}multipart/form-data, replaces the whole skilldeleteDELETE /v2/skills/{bucket}/{path}create_folderPUT /v2/skills/{bucket}/{path}/400if it already existsdelete_folderDELETE /v2/skills/{bucket}/{path}/409if not emptyupload_filePUT /v2/skills/{bucket}/{path}/files/{filePath}delete_fileDELETE /v2/skills/{bucket}/{path}/files/{filePath}SKILL.mdCore behaviour that must drive the design (checked against
epam/ai-dial-core@development—open_api_core.yamlis lossy here):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.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.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.DELETEsucceeds only if the folder is empty (409otherwise);GETon a trailing-slash path answers400by design — use the metadata listing from Skills: read side — get_metadata, list_files, get_file, download (/v2/skills) #136.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:
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 Skills: read side — get_metadata, list_files, get_file, download (/v2/skills) #136 — the children metadata listing does not expose it, so a write response is one of the few places to obtain it.upload_folder(url, local_dir)(walk a directory into multipart parts) anddownload_to(url, local_dir)(extract the ZIP from Skills: read side — get_metadata, list_files, get_file, download (/v2/skills) #136'sdownload) over stdlibzipfile+os.walk, or to keep the surface strictly byte-level and let callers do it.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), theIf-Matchcreate-vs-overwrite matrix,ETagsurfaced on every write,409on deleting a non-empty grouping folder, and400whenSKILL.mdis missing or its frontmatter lacksname/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 takesmultipart/form-datawith 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.saveisjson_data-shaped and returns a parsedPromptItem; every operation here is multipart- or ETag-header-shaped, sofiles.pyis the closer template — as it is for #136.