Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
50.00% |
32 / 64 |
|
60.00% |
3 / 5 |
CRAP | |
0.00% |
0 / 1 |
MvpImageRecommendationApiHandler | |
50.00% |
32 / 64 |
|
60.00% |
3 / 5 |
34.12 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getApiRequest | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
12 | |||
getSuggestionDataFromApiResponse | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
5.00 | |||
getConfidence | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
sortSuggestions | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\NewcomerTasks\AddImage; |
4 | |
5 | use GrowthExperiments\NewcomerTasks\TaskType\ImageRecommendationTaskTypeHandler; |
6 | use GrowthExperiments\NewcomerTasks\TaskType\TaskType; |
7 | use MediaWiki\Context\RequestContext; |
8 | use MediaWiki\Http\HttpRequestFactory; |
9 | use MediaWiki\Title\Title; |
10 | use StatusValue; |
11 | |
12 | /** |
13 | * Handler for MVP image suggestion API. |
14 | * Documentation: https://image-suggestion-api.wmcloud.org/?doc#/Image%20Suggestions |
15 | * This API should not be further used in production. |
16 | * Configuration of constructor parameters: |
17 | * - $url: GEImageRecommendationServiceUrl |
18 | * - $proxyUrl: GEImageRecommendationServiceHttpProxy |
19 | * - $useTitles: GEImageRecommendationServiceUseTitles |
20 | */ |
21 | class MvpImageRecommendationApiHandler implements ImageRecommendationApiHandler { |
22 | |
23 | /** @var HttpRequestFactory */ |
24 | private $httpRequestFactory; |
25 | |
26 | /** @var string */ |
27 | private $url; |
28 | |
29 | /** @var string|null */ |
30 | private $proxyUrl; |
31 | |
32 | /** @var string */ |
33 | private $wikiProject; |
34 | |
35 | /** @var string */ |
36 | private $wikiLanguage; |
37 | |
38 | /** @var int|null */ |
39 | private $requestTimeout; |
40 | |
41 | /** @var bool */ |
42 | private $useTitles; |
43 | |
44 | private const CONFIDENCE_RATING_TO_NUMBER = [ |
45 | 'high' => 3, |
46 | 'medium' => 2, |
47 | 'low' => 1 |
48 | ]; |
49 | |
50 | /** |
51 | * @param HttpRequestFactory $httpRequestFactory |
52 | * @param string $url Image recommendation service root URL |
53 | * @param string $wikiProject Wiki project (e.g. 'wikipedia') |
54 | * @param string $wikiLanguage Wiki language code |
55 | * @param string|null $proxyUrl HTTP proxy to use for $url |
56 | * @param int|null $requestTimeout Service request timeout in seconds |
57 | * @param bool $useTitles Use titles (the /:wiki/:lang/pages/:title API endpoint) |
58 | * instead of IDs (the /:wiki/:lang/pages endpoint)? |
59 | */ |
60 | public function __construct( |
61 | HttpRequestFactory $httpRequestFactory, |
62 | string $url, |
63 | string $wikiProject, |
64 | string $wikiLanguage, |
65 | ?string $proxyUrl, |
66 | ?int $requestTimeout, |
67 | bool $useTitles = false |
68 | ) { |
69 | $this->httpRequestFactory = $httpRequestFactory; |
70 | $this->url = $url; |
71 | $this->proxyUrl = $proxyUrl; |
72 | $this->wikiProject = $wikiProject; |
73 | $this->wikiLanguage = $wikiLanguage; |
74 | $this->requestTimeout = $requestTimeout; |
75 | $this->useTitles = $useTitles; |
76 | } |
77 | |
78 | /** @inheritDoc */ |
79 | public function getApiRequest( Title $title, TaskType $taskType ) { |
80 | if ( !$this->url ) { |
81 | return StatusValue::newFatal( 'rawmessage', |
82 | 'Image Suggestions API URL is not configured' ); |
83 | } |
84 | |
85 | $pathArgs = [ |
86 | 'image-suggestions', |
87 | 'v0', |
88 | $this->wikiProject, |
89 | $this->wikiLanguage, |
90 | 'pages', |
91 | ]; |
92 | $queryArgs = [ |
93 | 'source' => 'ima', |
94 | ]; |
95 | if ( $this->useTitles ) { |
96 | $pathArgs[] = $title->getPrefixedDBkey(); |
97 | } else { |
98 | $queryArgs['id'] = $title->getArticleID(); |
99 | } |
100 | $request = $this->httpRequestFactory->create( |
101 | wfAppendQuery( |
102 | $this->url . '/' . implode( '/', array_map( 'rawurlencode', $pathArgs ) ), |
103 | $queryArgs |
104 | ), |
105 | [ |
106 | 'method' => 'GET', |
107 | 'proxy' => $this->proxyUrl, |
108 | 'originalRequest' => RequestContext::getMain()->getRequest(), |
109 | 'timeout' => $this->requestTimeout, |
110 | ], |
111 | __METHOD__ |
112 | ); |
113 | $request->setHeader( 'Accept', 'application/json' ); |
114 | return $request; |
115 | } |
116 | |
117 | /** @inheritDoc */ |
118 | public function getSuggestionDataFromApiResponse( array $apiResponse, TaskType $taskType ): array { |
119 | if ( $taskType->getId() !== ImageRecommendationTaskTypeHandler::TASK_TYPE_ID ) { |
120 | // The MVP API does not provide section-level recommendations. |
121 | return []; |
122 | } |
123 | |
124 | if ( !$apiResponse['pages'] || !$apiResponse['pages'][0]['suggestions'] ) { |
125 | return []; |
126 | } |
127 | $imageData = []; |
128 | $sortedSuggestions = $this->sortSuggestions( $apiResponse['pages'][0]['suggestions'] ); |
129 | foreach ( $sortedSuggestions as $suggestion ) { |
130 | $filename = $suggestion['filename'] ?? null; |
131 | $source = $suggestion['source']['details']['from'] ?? null; |
132 | $projects = $suggestion['source']['details']['found_on'] ?? null; |
133 | $datasetId = $suggestion['source']['details']['dataset_id'] ?? null; |
134 | $imageData[] = new ImageRecommendationData( |
135 | $filename, |
136 | $source, |
137 | $projects, |
138 | $datasetId |
139 | ); |
140 | } |
141 | return $imageData; |
142 | } |
143 | |
144 | /** |
145 | * Get numeric value of the suggestion's confidence rating |
146 | * |
147 | * @param array $suggestion |
148 | * @return int |
149 | */ |
150 | private function getConfidence( array $suggestion ): int { |
151 | if ( array_key_exists( 'confidence_rating', $suggestion ) ) { |
152 | return self::CONFIDENCE_RATING_TO_NUMBER[$suggestion['confidence_rating']] ?? 0; |
153 | } |
154 | return 0; |
155 | } |
156 | |
157 | /** |
158 | * Sort the suggestions in decreasing order based on confidence rating |
159 | * |
160 | * @param array $suggestions |
161 | * @return array |
162 | */ |
163 | private function sortSuggestions( array $suggestions ): array { |
164 | $compare = function ( array $a, array $b ) { |
165 | return $this->getConfidence( $a ) < $this->getConfidence( $b ) ? 1 : -1; |
166 | }; |
167 | usort( $suggestions, $compare ); |
168 | return $suggestions; |
169 | } |
170 | } |