Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce improvements to AI process #651

Merged
merged 13 commits into from
Jan 31, 2025
5 changes: 4 additions & 1 deletion i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,12 @@
"requestwiki": "Request a wiki",
"requestwiki-ai-decision-approve": "Automatically, with the reasoning: $1",
"requestwiki-ai-decision-decline": "This request could not be approved at this time for the following reason: $1",
"requestwiki-ai-decision-dryrun": "This is an experimental AI analysis. Wiki requesters can safely ignore this.\n\n'''Recommendation''': $1.\n\n'''Reasoning''': $2",
"requestwiki-ai-decision-dryrun": "This is an experimental AI analysis. Wiki requesters can safely ignore this.\n\n'''Recommendation''': $1.\n\n'''Reasoning''': $2\n\n'''Confidence''': $3%",
BlankEclair marked this conversation as resolved.
Show resolved Hide resolved
"requestwiki-ai-decision-moredetails": "This request requires more details before it can be evaluated any further, for the following reason: $1",
"requestwiki-ai-decision-onhold": "This request could not be automatically approved and requires a manual review.",
"requestwiki-ai-error": "This request could not be automatically evaluated. Contact your system administrator for further information.",
"requestwiki-ai-error-reason": "This request could not be automatically evaluated and the reason has been logged. Contact your system administrator for further information.",
"requestwiki-ai-error-history-reason": "The following error was returned when evaluating the request through artificial intelligence:\n\n'$1'",
"requestwiki-edit-success": "Your changes to this wiki request have been successfully saved.",
"requestwiki-error-emailnotconfirmed": "Your email is not confirmed. To request wikis, please [[Special:ChangeEmail|confirm an email]] first.",
"requestwiki-error-invalidcomment": "Your wiki request contains an invalid character. Please correct that and try again.",
Expand Down
3 changes: 3 additions & 0 deletions i18n/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@
"requestwiki-ai-decision-dryrun": "Comment added to wiki requests when AI approvals are on dry run mode. Contains outcome and the reasoning for the action.",
"requestwiki-ai-decision-moredetails": "Comment added to wiki requests upon more details being requested containing the reasoning for requesting such details.",
"requestwiki-ai-decision-onhold": "Comment added to wiki requests upon being placed on hold containing the reasoning for such action.",
"requestwiki-ai-error": "Comment added to wiki requests upon a fatal, uncoverable error when attempting to contact the AI.",
"requestwiki-ai-error-reason": "Comment added to wiki requests upon a fatal, uncoverable error is returned by the AI.",
"requestwiki-ai-error-history-reason": "Comment added to wiki request histories upon encountering an error contacting the AI.",
"requestwiki-edit-success": "Success message displayed when a wiki request is successfully edited.",
"requestwiki-error-emailnotconfirmed": "Error message shown when the user's email is not confirmed during a wiki request.",
"requestwiki-error-invalidcomment": "Invalid comment/character based on disallowed error message.",
Expand Down
154 changes: 104 additions & 50 deletions includes/Jobs/RequestWikiRemoteAIJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ class RequestWikiRemoteAIJob extends Job {
private bool $bio;
private bool $private;
private string $category;
private bool $nsfw;
private string $nsfwtext;
private array $extraData;

public function __construct(
array $params,
Expand All @@ -58,16 +57,6 @@ public function __construct(
$this->apiKey = $this->config->get( ConfigNames::OpenAIConfig )['apikey'] ?? '';

$this->id = $params['id'];
$this->reason = $params['reason'];
$this->sitename = $params['sitename'];
$this->subdomain = $params['subdomain'];
$this->username = $params['username'];
$this->language = $params['language'];
$this->bio = $params['bio'];
$this->private = $params['private'];
$this->category = $params['category'];
$this->nsfw = $params['nsfw'];
$this->nsfwtext = $params['nsfwtext'];
}

public function run(): bool {
Expand Down Expand Up @@ -108,48 +97,89 @@ public function run(): bool {
);

$apiResponse = $this->queryOpenAI(
$this->sitename,
$this->subdomain,
$this->reason,
$this->username,
$this->language,
$this->bio,
$this->private,
$this->category,
$this->nsfw,
$this->nsfwtext
$this->wikiRequestManager->isBio(),
$this->wikiRequestManager->getCategory(),
$this->wikiRequestManager->getAllExtraData(),
$this->wikiRequestManager->getLanguage(),
$this->wikiRequestManager->isPrivate(),
$this->wikiRequestManager->getReason(),
$this->wikiRequestManager->getSitename(),
substr( $this->wikiRequestManager->getDBname(), 0, -4 ),
$this->wikiRequestManager->getRequesterUsername(),
count( $this->wikiRequestManager->getVisibleRequestsByUser(
$this->wikiRequestManager->getRequester(), User::newSystemUser( 'CreateWiki AI' )
) )
);

if ( !$apiResponse ) {
$commentText = $this->context->msg( 'requestwiki-ai-error' )
->inContentLanguage()
->parse();

$this->wikiRequestManager->addComment(
comment: $commentText,
user: User::newSystemUser( 'CreateWiki AI' ),
log: false,
type: 'comment',
notifyUsers: []
);

return true;
}

if ( $apiResponse['error'] ) {
$publicCommentText = $this->context->msg( 'requestwiki-ai-error-reason' )
->inContentLanguage()
->parse();

$requestHistoryComment = $this->context->msg( 'requestwiki-ai-error-history-reason' )
->params( $apiResponse['error'] )
->inContentLanguage()
->parse();

$this->wikiRequestManager->addRequestHistory(
action: 'ai-error',
details: $requestHistoryComment,
user: User::newSystemUser( 'CreateWiki AI' )
);

$this->wikiRequestManager->addComment(
comment: $publicCommentText,
user: User::newSystemUser( 'CreateWiki AI' ),
log: false,
type: 'comment',
notifyUsers: []
);
}

// Extract response details with default fallbacks
$confidence = $apiResponse['recommendation']['confidence'] ?? '0';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be casted to an int, otherwise the functions that take this in will die if they're type hinted

$outcome = $apiResponse['recommendation']['outcome'] ?? 'reject';
$comment = $apiResponse['recommendation']['public_comment'] ?? 'No comment provided. Please check logs.';

$this->logger->debug(
'AI decision for wiki request {id} was {outcome} with reasoning: {comment}',
'AI decision for wiki request {id} was {outcome} (with {confidence}% confidence) with reasoning: {comment}',
[
'comment' => $comment,
'confidence' => $confidence,
'id' => $this->id,
'outcome' => $outcome,
]
);

if ( $this->config->get( ConfigNames::OpenAIConfig )['dryrun'] ) {
return $this->handleDryRun( $outcome, $comment );
return $this->handleDryRun( $outcome, $comment, $confidence );
}

return $this->handleLiveRun( $outcome, $comment );
return $this->handleLiveRun( $outcome, $comment, $confidence );
}

private function handleDryRun( string $outcome, string $comment ): bool {
private function handleDryRun( string $outcome, string $comment, int $confidence ): bool {
$outcomeMessage = $this->context->msg( 'requestwikiqueue-' . $outcome )->text();
$commentText = $this->context->msg( 'requestwiki-ai-decision-dryrun' )
->params( $outcomeMessage, $comment )
->inContentLanguage()
->parse();
->params( $outcomeMessage, $comment, $confidence )
->inContentLanguage()
->parse();

$this->wikiRequestManager->addComment(
comment: $commentText,
Expand Down Expand Up @@ -178,10 +208,10 @@ private function handleDryRun( string $outcome, string $comment ): bool {
return true;
}

private function handleLiveRun( string $outcome, string $comment ): bool {
private function handleLiveRun( string $outcome, string $comment, int $confidence ): bool {
$systemUser = User::newSystemUser( 'CreateWiki AI' );
$commentText = $this->context->msg( 'requestwiki-ai-decision-' . $outcome )
->params( $comment )
->params( $comment, $confidence )
->inContentLanguage()
->parse();

Expand All @@ -194,9 +224,11 @@ private function handleLiveRun( string $outcome, string $comment ): bool {
);
$this->wikiRequestManager->tryExecuteQueryBuilder();
$this->logger->debug(
'Wiki request {id} was automatically approved by AI decision with reason: {comment}',
'Wiki request {id} was automatically approved by AI decision ' .
'(with {confidence}% confidence) with reason: {comment}',
[
'comment' => $comment,
'confidence' => $confidence,
'id' => $this->id,
]
);
Expand Down Expand Up @@ -272,28 +304,50 @@ private function handleLiveRun( string $outcome, string $comment ): bool {
}

private function queryOpenAI(
bool $bio,
string $category,
array $extraData,
string $language,
bool $private,
string $reason,
string $sitename,
string $subdomain,
string $reason,
string $username,
string $language,
bool $bio,
bool $private,
string $category,
bool $nsfw,
string $nsfwtext
int $userRequestsNum
): ?array {
try {
$isBio = $bio ? "Yes" : "No";
$isFork = !empty( $extraData['source'] ) ? "Yes" : "No";
$isNsfw = !empty( $extraData['nsfw'] ) ? "Yes" : "No";
$isPrivate = $private ? "Yes" : "No";
$isNsfw = $nsfw ? "Yes" . $nsfwtext : "No";
$nsfwReasonText = $nsfw ? "What type of NSFW content will it feature? '$nsfwtext'. " : "";

$sanitizedReason = "Wiki name: '$sitename'. Subdomain: '$subdomain'. Requester: '$username'. " .
"Language: '$language'. Focuses on real people/groups? '$isBio'. Private wiki? '$isPrivate'. " .
"Category: '$category'. Contains content that is not safe for work? '$isNsfw'. " .
$nsfwReasonText . "Wiki request reason: " .
trim( str_replace( [ "\r\n", "\r" ], "\n", $reason ) );
$forkText = !empty( $extraData['sourceurl'] )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we bringing html into this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra security against inputs in RequestWiki. Do you think this is not necessary?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use str_replace( '\'', '\\\'', $text ) (and escape backslashes, newlines, potentially control characters, etc)

Though, even escaping is not enough (for example, clearly delinated user input can be interpreted as instructions, and this still can apply to the chat API)

? "This wiki is forking from this URL: \"" .
htmlspecialchars( $extraData['sourceurl'], ENT_QUOTES ) . "\". "
: "";
$nsfwReasonText = !empty( $extraData['nsfwtext'] )
? "What type of NSFW content will it feature? \"" .
htmlspecialchars( $extraData['nsfwtext'], ENT_QUOTES ) . "\". "
: "";

$sanitizedReason = sprintf(
'Wiki name: "%s". Subdomain: "%s". Requester: "%s". ' .
'Number of previous requests: "%d". Language: "%s". ' .
'Focuses on real people/groups? "%s". Private wiki? "%s". Category: "%s". ' .
'Contains content that is not safe for work? "%s". %s%s' .
'Wiki request description: %s',
htmlspecialchars( $sitename, ENT_QUOTES ),
htmlspecialchars( $subdomain, ENT_QUOTES ),
htmlspecialchars( $username, ENT_QUOTES ),
$userRequestsNum,
htmlspecialchars( $language, ENT_QUOTES ),
$isBio,
$isPrivate,
htmlspecialchars( $category, ENT_QUOTES ),
$isNsfw,
$nsfwReasonText,
$forkText,
htmlspecialchars( trim( str_replace( [ "\r\n", "\r" ], "\n", $reason ) ), ENT_QUOTES )
);

// Step 1: Create a new thread
$threadData = $this->createRequest( '/threads', 'POST', [
Expand All @@ -318,7 +372,7 @@ private function queryOpenAI(
if ( !$threadId ) {
$this->logger->error( 'OpenAI did not return a threadId!' );
$this->setLastError( 'Run ' . $this->id . ' failed. No threadId returned.' );
return null;
return $threadData;
}

// Step 2: Run the message
Expand Down Expand Up @@ -346,7 +400,7 @@ private function queryOpenAI(
if ( !$runId ) {
$this->logger->error( 'OpenAI did not return a runId!' );
$this->setLastError( 'Run ' . $this->id . ' failed. No runId returned.' );
return null;
return $runData;
}

// Step 3: Poll the status of the run
Expand Down Expand Up @@ -391,7 +445,7 @@ private function queryOpenAI(

$this->setLastError( 'Run ' . $runId . ' failed.' );

return null;
return $statusData;
}
}

Expand Down
47 changes: 13 additions & 34 deletions includes/Services/WikiRequestManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,17 +159,7 @@ public function createNewRequestAndLog(
$this->options->get( ConfigNames::OpenAIConfig )['apikey'] &&
$this->options->get( ConfigNames::OpenAIConfig )['assistantid']
) {
$this->evaluateWithOpenAI( $data['sitename'],
$data['subdomain'],
$data['reason'],
$user->getName(),
$data['language'],
$data['bio'],
$data['private'] ?? 0,
$data['category'] ?? '',
$extraData['nsfw'] ?? 0,
$extraData['nsfwtext'] ?? ''
);
$this->evaluateWithOpenAI();
}

$this->logNewRequest( $data, $user );
Expand Down Expand Up @@ -377,6 +367,12 @@ public function isVisibilityAllowed( int $visibility, UserIdentity $user ): bool
return true;
}

// CreateWiki AI should be able to see this.
// Additionally, the username is reserved.
if ( $user->getName() === 'CreateWiki AI' ) {
return true;
}

return $this->permissionManager->userHasRight(
$user, self::VISIBILITY_CONDS[$visibility]
);
Expand Down Expand Up @@ -678,6 +674,10 @@ public function getRequester(): User {
return $this->userFactory->newFromId( $this->row->cw_user );
}

public function getRequesterUsername(): string {
return $this->userFactory->newFromId( $this->row->cw_user )->getName();
}

public function getStatus(): string {
return $this->row->cw_status;
}
Expand Down Expand Up @@ -982,34 +982,13 @@ private function tryAutoCreate( string $reason ): void {
);
}

private function evaluateWithOpenAI(
string $sitename,
string $subdomain,
string $reason,
string $username,
string $language,
bool $bio,
bool $private,
string $category,
bool $nsfw,
string $nsfwtext
): void {
private function evaluateWithOpenAI(): void {
$jobQueueGroup = $this->jobQueueGroupFactory->makeJobQueueGroup();
$jobQueueGroup->push(
new JobSpecification(
RequestWikiRemoteAIJob::JOB_NAME,
[
'id' => $this->ID,
'sitename' => $sitename,
'subdomain' => $subdomain,
'reason' => $reason,
'username' => $username,
'language' => $language,
'bio' => $bio,
'private' => $private,
'category' => $category,
'nsfw' => $nsfw,
'nsfwtext' => $nsfwtext
'id' => $this->ID
]
)
);
Expand Down
Loading