Skip to content

Commit 5f9b77a

Browse files
authored
Merge branch 'main' into regenerate-readme
2 parents dd60640 + 6a147ac commit 5f9b77a

10 files changed

Lines changed: 1066 additions & 15 deletions
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: "Copilot Setup Steps"
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
paths:
7+
- .github/workflows/copilot-setup-steps.yml
8+
pull_request:
9+
paths:
10+
- .github/workflows/copilot-setup-steps.yml
11+
12+
jobs:
13+
copilot-setup-steps:
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v6
21+
22+
- name: Check existence of composer.json file
23+
id: check_composer_file
24+
uses: andstor/file-existence-action@v3
25+
with:
26+
files: "composer.json"
27+
28+
- name: Set up PHP environment
29+
if: steps.check_composer_file.outputs.files_exists == 'true'
30+
uses: shivammathur/setup-php@v2
31+
with:
32+
php-version: 'latest'
33+
ini-values: zend.assertions=1, error_reporting=-1, display_errors=On
34+
coverage: 'none'
35+
tools: composer,cs2pr
36+
env:
37+
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38+
39+
- name: Install Composer dependencies & cache dependencies
40+
if: steps.check_composer_file.outputs.files_exists == 'true'
41+
uses: ramsey/composer-install@v3
42+
env:
43+
COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }}
44+
with:
45+
# Bust the cache at least once a month - output format: YYYY-MM.
46+
custom-cache-suffix: $(date -u "+%Y-%m")

.github/workflows/testing.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ on:
1313
jobs:
1414
test:
1515
uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main
16+
with:
17+
minimum-php: '7.4'

ai-command.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
}
1616

1717
WP_CLI::add_command( 'ai', AI_Command::class );
18+
WP_CLI::add_command( 'ai credentials', Credentials_Command::class );

composer.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,15 @@
2727
},
2828
"bundled": false,
2929
"commands": [
30-
"ai"
30+
"ai",
31+
"ai credentials",
32+
"ai credentials get",
33+
"ai credentials set",
34+
"ai credentials delete",
35+
"ai credentials list",
36+
"ai check",
37+
"ai generate",
38+
"ai status"
3139
]
3240
},
3341
"autoload": {

features/credentials.feature

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
Feature: Manage AI provider credentials
2+
3+
Background:
4+
Given a WP install
5+
6+
Scenario: List credentials when none exist
7+
When I run `wp ai credentials list`
8+
Then STDOUT should contain:
9+
"""
10+
No credentials found.
11+
"""
12+
13+
Scenario: Set and list credentials
14+
When I run `wp ai credentials set openai --api-key=sk-test123456789`
15+
Then STDOUT should contain:
16+
"""
17+
Success: Credentials for provider "openai" have been saved.
18+
"""
19+
20+
When I run `wp ai credentials list --format=json`
21+
Then STDOUT should be JSON containing:
22+
"""
23+
[{"provider":"openai","api_key":"sk-*********6789"}]
24+
"""
25+
26+
Scenario: Get specific provider credentials
27+
When I run `wp ai credentials set anthropic --api-key=sk-ant-api-key-123`
28+
Then STDOUT should contain:
29+
"""
30+
Success: Credentials for provider "anthropic" have been saved.
31+
"""
32+
33+
When I run `wp ai credentials get anthropic --format=json`
34+
Then STDOUT should contain:
35+
"""
36+
"provider":"anthropic"
37+
"""
38+
And STDOUT should contain:
39+
"""
40+
"api_key":"sk-**********-123"
41+
"""
42+
43+
Scenario: Delete provider credentials
44+
When I run `wp ai credentials set google --api-key=test-google-key`
45+
Then STDOUT should contain:
46+
"""
47+
Success: Credentials for provider "google" have been saved.
48+
"""
49+
50+
When I run `wp ai credentials delete google`
51+
Then STDOUT should contain:
52+
"""
53+
Success: Credentials for provider "google" have been deleted.
54+
"""
55+
56+
When I try `wp ai credentials get google`
57+
Then STDERR should contain:
58+
"""
59+
Error: Credentials for provider "google" not found.
60+
"""
61+
And the return code should be 1
62+
63+
Scenario: Error when getting non-existent credentials
64+
When I try `wp ai credentials get nonexistent`
65+
Then STDERR should contain:
66+
"""
67+
Error: Credentials for provider "nonexistent" not found.
68+
"""
69+
And the return code should be 1
70+
71+
Scenario: Error when setting credentials without api-key
72+
When I try `wp ai credentials set openai`
73+
Then STDERR should contain:
74+
"""
75+
missing --api-key parameter
76+
"""
77+
And the return code should be 1
78+
79+
Scenario: List multiple credentials in table format
80+
When I run `wp ai credentials set openai --api-key=sk-openai123`
81+
And I run `wp ai credentials set anthropic --api-key=sk-ant-api-456`
82+
And I run `wp ai credentials list`
83+
Then STDOUT should be a table containing rows:
84+
| provider | api_key |
85+
| openai | sk-*****i123 |
86+
| anthropic | sk-*******-456 |
87+
88+
Scenario: Update existing credentials
89+
When I run `wp ai credentials set openai --api-key=old-key-123`
90+
Then STDOUT should contain:
91+
"""
92+
Success: Credentials for provider "openai" have been saved.
93+
"""
94+
95+
When I run `wp ai credentials set openai --api-key=new-key-456`
96+
Then STDOUT should contain:
97+
"""
98+
Success: Credentials for provider "openai" have been saved.
99+
"""
100+
101+
When I run `wp ai credentials get openai --format=json`
102+
Then STDOUT should contain:
103+
"""
104+
"api_key":"new****-456"
105+
"""

features/generate.feature

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
Feature: Generate AI content
2+
3+
Background:
4+
Given a WP install
5+
And a wp-content/mu-plugins/mock-provider.php file:
6+
"""
7+
<?php
8+
9+
use WordPress\AiClient\AiClient;
10+
use WordPress\AiClient\Messages\DTO\MessagePart;
11+
use WordPress\AiClient\Messages\DTO\ModelMessage;
12+
use WordPress\AiClient\Messages\Enums\ModalityEnum;
13+
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
14+
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
15+
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
16+
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
17+
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
18+
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
19+
use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
20+
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
21+
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
22+
use WordPress\AiClient\Providers\Models\DTO\SupportedOption;
23+
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
24+
use WordPress\AiClient\Providers\Models\Enums\OptionEnum;
25+
use WordPress\AiClient\Results\DTO\Candidate;
26+
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
27+
use WordPress\AiClient\Results\DTO\TokenUsage;
28+
use WordPress\AiClient\Results\Enums\FinishReasonEnum;
29+
use WordPress\AI_Client\API_Credentials\API_Credentials_Manager;
30+
31+
class WP_CLI_Mock_Model implements ModelInterface, TextGenerationModelInterface {
32+
private $id;
33+
private $config;
34+
35+
public function __construct( $id ) {
36+
$this->id = $id;
37+
$this->config = new ModelConfig();
38+
}
39+
40+
public function metadata(): ModelMetadata {
41+
return new ModelMetadata(
42+
$this->id,
43+
'WP-CLI Mock Model',
44+
// Supported capabilities.
45+
[
46+
CapabilityEnum::textGeneration(),
47+
CapabilityEnum::imageGeneration(),
48+
],
49+
// Supported options.
50+
[
51+
new SupportedOption(
52+
OptionEnum::inputModalities(),
53+
[
54+
[ModalityEnum::text()]
55+
]
56+
),
57+
new SupportedOption(
58+
OptionEnum::outputModalities(),
59+
[
60+
[ModalityEnum::text()],
61+
[ModalityEnum::text(), ModalityEnum::image()],
62+
]
63+
),
64+
]
65+
);
66+
}
67+
68+
public function providerMetadata(): ProviderMetadata {
69+
return WP_CLI_Mock_Provider::metadata();
70+
}
71+
72+
public function setConfig( ModelConfig $config ): void {
73+
$this->config = $config;
74+
}
75+
76+
public function getConfig(): ModelConfig {
77+
return $this->config;
78+
}
79+
80+
public function generateTextResult(array $prompt): GenerativeAiResult {
81+
// throw new RuntimeException('No candidates were generated');
82+
83+
$modelMessage = new ModelMessage([
84+
new MessagePart('Generated content')
85+
]);
86+
$candidate = new Candidate(
87+
$modelMessage,
88+
FinishReasonEnum::stop(),
89+
42
90+
);
91+
$tokenUsage = new TokenUsage(10, 42, 52);
92+
return new GenerativeAiResult(
93+
'result_123',
94+
[ $candidate ],
95+
$tokenUsage,
96+
$this->providerMetadata(),
97+
$this->metadata(),
98+
[ 'provider' => 'wp-cli-mock-provider' ]
99+
);
100+
}
101+
102+
public function streamGenerateTextResult(array $prompt): Generator {
103+
yield from [];
104+
}
105+
}
106+
107+
class WP_CLI_Mock_Provider implements ProviderInterface {
108+
public static function metadata(): ProviderMetadata {
109+
return new ProviderMetadata( 'wp-cli-mock-provider', 'WP-CLI Mock Provider', ProviderTypeEnum::cloud() );
110+
}
111+
112+
public static function model( string $modelId, ?ModelConfig $modelConfig = null ): ModelInterface {
113+
return new WP_CLI_Mock_Model( $modelId );
114+
}
115+
116+
public static function availability(): ProviderAvailabilityInterface {
117+
return new class() implements ProviderAvailabilityInterface {
118+
public function isConfigured(): bool {
119+
return true;
120+
}
121+
};
122+
}
123+
124+
public static function modelMetadataDirectory(): ModelMetadataDirectoryInterface {
125+
return new class() implements ModelMetadataDirectoryInterface {
126+
public function listModelMetadata(): array {
127+
return [
128+
( new WP_CLI_Mock_Model( 'wp-cli-mock-model' ) )->metadata()
129+
];
130+
}
131+
132+
public function hasModelMetadata( string $modelId ): bool {
133+
return true;
134+
}
135+
136+
public function getModelMetadata( string $modelId ): ModelMetadata {
137+
return self::model()->metadata();
138+
}
139+
};
140+
}
141+
}
142+
143+
WP_CLI::add_hook(
144+
'ai_client_init',
145+
static function () {
146+
AiClient::defaultRegistry()->registerProvider( WP_CLI_Mock_Provider::class );
147+
148+
( new API_Credentials_Manager() )->initialize();
149+
}
150+
);
151+
"""
152+
153+
Scenario: Generate command validates model format
154+
When I try `wp ai generate text "Test prompt" --model=invalidformat`
155+
Then the return code should be 1
156+
157+
Scenario: Generate command validates max-tokens
158+
When I try `wp ai generate text "Test prompt" --max-tokens=-5`
159+
Then the return code should be 1
160+
And STDERR should contain:
161+
"""
162+
Max tokens must be a positive integer
163+
"""
164+
165+
Scenario: Generate command validates top-p range
166+
When I try `wp ai generate text "Test prompt" --top-p=1.5`
167+
Then the return code should be 1
168+
And STDERR should contain:
169+
"""
170+
Top-p must be between 0.0 and 1.0
171+
"""
172+
173+
Scenario: Generate command validates top-k positive
174+
When I try `wp ai generate text "Test prompt" --top-k=-10`
175+
Then the return code should be 1
176+
And STDERR should contain:
177+
"""
178+
Top-k must be a positive integer
179+
"""

phpcs.xml.dist

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?xml version="1.0"?>
2+
<ruleset name="WP-CLI-ai-command">
3+
<description>Custom ruleset for WP-CLI ai-command</description>
4+
5+
<!-- What to scan. -->
6+
<file>.</file>
7+
8+
<!-- Show progress. -->
9+
<arg value="p"/>
10+
11+
<!-- Strip the filepaths down to the relevant bit. -->
12+
<arg name="basepath" value="./"/>
13+
14+
<!-- Check up to 8 files simultaneously. -->
15+
<arg name="parallel" value="8"/>
16+
17+
<!-- USE THE WP_CLI_CS RULESET -->
18+
<rule ref="WP_CLI_CS"/>
19+
20+
<!-- PROJECT SPECIFIC CONFIGURATION -->
21+
22+
<!-- For help understanding the `testVersion` configuration setting:
23+
https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions -->
24+
<config name="testVersion" value="7.4-"/>
25+
26+
<!-- Verify that everything in the global namespace is either namespaced or prefixed -->
27+
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
28+
<properties>
29+
<property name="prefixes" type="array">
30+
<element value="WP_CLI\AI"/><!-- Namespaces. -->
31+
<element value="wpcli_ai"/><!-- Global variables and such. -->
32+
</property>
33+
</properties>
34+
</rule>
35+
36+
</ruleset>

0 commit comments

Comments
 (0)