Skip to content
Merged
102 changes: 102 additions & 0 deletions .github/scripts/agent-skill-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Sets the `Agent Skill` commit status on a PR and keeps its sticky comment in
// sync. Called from .github/workflows/agent-skill.yml via actions/github-script.

const SKILL_DIR = 'src/rapidata/_skill/';
const MARKER = '<!-- agent-skill-check -->';

// Must stay a superset of the paths automerge-openapi-client.yml accepts, or
// the daily generator PR is left with a failing status nobody reviews.
const GENERATED = /^(openapi\/|src\/rapidata\/api_client\/|src\/rapidata\/api_client_README\.md$)/;
const VERSION_BUMP_FILES = new Set(['pyproject.toml', 'src/rapidata/__init__.py']);
const VERSION_BUMP_MESSAGE = /^Bump version from \S+ to \S+/;

function classify(paths, commitMessages) {
if (paths.some((p) => p.startsWith(SKILL_DIR))) return 'changed';
if (paths.length > 0 && paths.every((p) => GENERATED.test(p))) return 'generated';
if (
paths.length > 0 &&
paths.every((p) => VERSION_BUMP_FILES.has(p)) &&
commitMessages.length > 0 &&
commitMessages.every((m) => VERSION_BUMP_MESSAGE.test(m))
) {
return 'version-bump';
}
return 'unchanged';
}

function commentBody(outcome, ackLabel, actor) {
const how = [
`This PR does not modify \`${SKILL_DIR}\`. That directory is the agent skill that ships in every SDK release and that coding agents read through \`python -m rapidata skill\`.`,
'',
'Before merging, pick one:',
`1. The change affects what an agent needs to know (new or renamed API, changed parameter, default or result field, new gotcha): update \`${SKILL_DIR}SKILL.md\` (or a companion guide next to it) in this PR.`,
`2. Nothing the skill documents changed: a reviewer applies the \`${ackLabel}\` label. New commits remove the label again.`,
];
if (outcome === 'acknowledged') {
return [MARKER, `### ✅ No skill update needed — confirmed by @${actor}`, '', ...how].join('\n');
}
if (outcome === 'resolved') {
return [MARKER, '### ✅ Agent skill check passed', '', 'The skill was updated or the PR only touches generated or release files.'].join('\n');
}
return [MARKER, '### ⚠ Agent skill not updated', '', ...how].join('\n');
}

module.exports = async function run({ github, context, core, ackLabel, statusContext }) {
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const action = context.payload.action;

if (action === 'synchronize') {
try {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: ackLabel });
core.info(`Removed ${ackLabel}: new commits need a fresh confirmation.`);
} catch (e) {
if (e.status !== 404) throw e;
}
}

const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: pr.number, per_page: 100 });
const paths = files.flatMap((f) => [f.filename, f.previous_filename].filter(Boolean));
const commits = await github.paginate(github.rest.pulls.listCommits, { owner, repo, pull_number: pr.number, per_page: 100 });
const kind = classify(paths, commits.map((c) => c.commit.message));

const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: pr.number });
const labelPresent = issue.labels.some((l) => (typeof l === 'string' ? l : l.name) === ackLabel);

let state, description, outcome;
if (kind === 'changed') {
[state, description, outcome] = ['success', 'Agent skill updated in this PR.', 'resolved'];
} else if (kind === 'generated') {
[state, description, outcome] = ['success', 'Generated API client only; no skill update needed.', 'resolved'];
} else if (kind === 'version-bump') {
[state, description, outcome] = ['success', 'Release version bump; no skill update needed.', 'resolved'];
} else if (labelPresent) {
[state, description, outcome] = ['success', `No skill update needed (${ackLabel}).`, 'acknowledged'];
} else {
[state, description, outcome] = ['failure', `Update ${SKILL_DIR}SKILL.md or apply ${ackLabel}.`, 'pending'];
}

const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
const existing = comments.find((c) => c.body && c.body.includes(MARKER));
const body = commentBody(outcome, ackLabel, context.actor);
if (existing) {
if (existing.body !== body) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
}
} else if (outcome !== 'resolved') {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
}

await github.rest.repos.createCommitStatus({
owner,
repo,
sha: pr.head.sha,
state,
context: statusContext,
description: description.slice(0, 140),
target_url: `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`,
});
core.info(`${statusContext}: ${state} (${kind}${labelPresent ? ', labeled' : ''})`);
};

module.exports.classify = classify;
53 changes: 53 additions & 0 deletions .github/workflows/agent-skill.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Agent Skill

# The agent skill in src/rapidata/_skill/ ships in every wheel and is edited only
# here. A PR that leaves it untouched needs a reviewer to confirm, with the ack
# label, that nothing the skill documents changed.
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]

env:
ACK_LABEL: skill-unchanged-approved
STATUS_CONTEXT: Agent Skill

permissions:
contents: read
issues: write
pull-requests: write
statuses: write

jobs:
check:
name: Check skill update
if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }}
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
- name: Evaluate
uses: actions/github-script@v7
with:
script: |
const run = require('./.github/scripts/agent-skill-check.js');
await run({ github, context, core, ackLabel: process.env.ACK_LABEL, statusContext: process.env.STATUS_CONTEXT });

ack:
name: Acknowledge label change
if: >-
(github.event.action == 'labeled' || github.event.action == 'unlabeled')
&& github.event.label.name == 'skill-unchanged-approved'
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
- name: Re-evaluate
uses: actions/github-script@v7
with:
script: |
const run = require('./.github/scripts/agent-skill-check.js');
await run({ github, context, core, ackLabel: process.env.ACK_LABEL, statusContext: process.env.STATUS_CONTEXT });
16 changes: 3 additions & 13 deletions .github/workflows/release_and_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,8 @@ jobs:
dist/*.whl
dist/*.tar.gz

# A repository_dispatch needs Contents:write on the *target* repo, so a single
# token has to cover every repo the release notifies. The GitHub App grants that
# per run; a repo-scoped PAT silently covers only the repos it was cut for.
# A repository_dispatch needs Contents:write on the *target* repo; the GitHub App
# grants that per run, where a repo-scoped PAT silently covers only its own repos.
- name: Create cross-repo dispatch token
id: dispatch_token
if: steps.release_type.outputs.type == 'stable'
Expand All @@ -183,18 +182,9 @@ jobs:
app-id: ${{ vars.RAPIDATA_OPENAPI_GENERATOR_APP_ID }}
private-key: ${{ secrets.RAPIDATA_OPENAPI_GENERATOR_PRIVATE_KEY }}
owner: RapidataAI
repositories: "skills,rapidata-mcp"
repositories: "rapidata-mcp"
permission-contents: write

- name: Trigger skills plugin version sync
if: steps.release_type.outputs.type == 'stable'
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ steps.dispatch_token.outputs.token }}
repository: RapidataAI/skills
event-type: sdk-release
client-payload: '{"version": "${{ steps.update_version.outputs.new_version }}"}'

# The hosted MCP server wraps this SDK but resolves it at image build time, so
# without a rebuild it keeps serving whatever version was current when that repo
# last changed. Hand it the version just published so it pins exactly this release.
Expand Down
37 changes: 37 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
This Project is a Python SDK for the Rapidata API.

It is built around the RapidataClient class which is the main entry point for interacting with the Rapidata API.

As a customer you can use the RapidataClient class to access the following:
- JobDefinitionCreation
- AudienceCreation (including filtered audiences via `.filter()` for country/language/demographic targeting)
- ValidationSetCreation
- FlowCreation
- BenchmarkCreation / MRI Creation (including participant metadata such as `participant.rename()` and `participant.set_price()` for the score-vs-cost chart)

Orders were removed from the SDK entirely (v3.21.0) — do not add, document, or reference order creation.

The whole authentication and backend communication is handled by the OpenAPIService class. It works in combination with the AUTO GENERATED API CLIENT that is used to make the actual API calls.

Note that if there are any changes that have to be made in those files you MUST also update the mustache files under openapi/templates.

please note that there is the RapidataApiClient that wraps every api call to handel backend tracing and error handling.

The backend errors follow a specific format that you can see in the RapidataError class.

when doing type annotations use the "from __future__ import annotations" statement and TYPE_CHECKING to check the types - that way you can eliminate the quotation marks around the types.

## Documentation
When building the docs make sure you use 'uv run --group docs mkdocs build' - otherwise check out the pyproject.toml file for the dependencies.

When writing documentation, make sure to keep focused, easy to understand, and not repeat information. it should highlight the capabilities while not overexaggerating or falling into hyperbole.


## General rules
at the end of your edits make sure to run 'pyright src/rapidata/rapidata_client' and make sure there are no errors.
when updating any interfaces make sure you update the docs and examples.
before every commit make sure to format everything under src/rapidata/rapidata_client with black ('uv run black src/rapidata/rapidata_client').

## Agent skill
`src/rapidata/_skill/` is the agent skill that ships in every release and that coding agents read through `python -m rapidata skill`. It is edited only here.
With every change, review whether `SKILL.md` or its companions (`reference.md`, `examples.md`, `flows-for-preference-data.md`) need updating: a new or renamed method, a changed parameter, default or result field, or a new pitfall all do. Update them in the same PR. If nothing the skill documents changed, say so in the PR body, so the reviewer can apply the `skill-unchanged-approved` label that the `Agent Skill` check waits for.
34 changes: 1 addition & 33 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,33 +1 @@
This Project is a Python SDK for the Rapidata API.

It is built around the RapidataClient class which is the main entry point for interacting with the Rapidata API.

As a customer you can use the RapidataClient class to access the following:
- JobDefinitionCreation
- AudienceCreation (including filtered audiences via `.filter()` for country/language/demographic targeting)
- ValidationSetCreation
- FlowCreation
- BenchmarkCreation / MRI Creation (including participant metadata such as `participant.rename()` and `participant.set_price()` for the score-vs-cost chart)

Orders were removed from the SDK entirely (v3.21.0) — do not add, document, or reference order creation.

The whole authentication and backend communication is handled by the OpenAPIService class. It works in combination with the AUTO GENERATED API CLIENT that is used to make the actual API calls.

Note that if there are any changes that have to be made in those files you MUST also update the mustache files under openapi/templates.

please note that there is the RapidataApiClient that wraps every api call to handel backend tracing and error handling.

The backend errors follow a specific format that you can see in the RapidataError class.

when doing type annotations use the "from __future__ import annotations" statement and TYPE_CHECKING to check the types - that way you can eliminate the quotation marks around the types.

## Documentation
When building the docs make sure you use 'uv run --group docs mkdocs build' - otherwise check out the pyproject.toml file for the dependencies.

When writing documentation, make sure to keep focused, easy to understand, and not repeat information. it should highlight the capabilities while not overexaggerating or falling into hyperbole.


## General rules
at the end of your edits make sure to run 'pyright src/rapidata/rapidata_client' and make sure there are no errors.
when updating any interfaces make sure you update the docs and examples.
before every commit make sure to format everything under src/rapidata/rapidata_client with black ('uv run black src/rapidata/rapidata_client').
@AGENTS.md
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ Docs: https://docs.rapidata.ai/

## Using a coding agent?

Point it at the maintained skill instead of letting it read the installed source:
Point it at the guide that ships with the SDK instead of letting it read the installed source:

```bash
python -m rapidata skill # print the guide
python -m rapidata skill --install # install it into the current project
```

The guide is edited in [`src/rapidata/_skill/`](https://github.com/RapidataAI/rapidata-python-sdk/tree/main/src/rapidata/_skill), so it always matches the installed version.

Details and per-agent install commands: https://docs.rapidata.ai/ai_agents/
Loading
Loading