robotframework-aivision 0.2.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- AIVision/__init__.py +29 -0
- AIVision/font/Anton-Regular.ttf +0 -0
- AIVision/font/OFL.txt +93 -0
- AIVision/genai.py +337 -0
- AIVision/library.py +437 -0
- AIVision/platforms.py +68 -0
- robotframework_aivision-0.2.0a1.dist-info/METADATA +163 -0
- robotframework_aivision-0.2.0a1.dist-info/RECORD +12 -0
- robotframework_aivision-0.2.0a1.dist-info/WHEEL +5 -0
- robotframework_aivision-0.2.0a1.dist-info/licenses/LICENSE +21 -0
- robotframework_aivision-0.2.0a1.dist-info/licenses/LICENSE.txt +21 -0
- robotframework_aivision-0.2.0a1.dist-info/top_level.txt +1 -0
AIVision/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Róbert Malovec
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
Main Robot Framework AI Library plugin entrypoint
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .library import AIVision
|
|
28
|
+
|
|
29
|
+
__all__ = ["AIVision"]
|
|
Binary file
|
AIVision/font/OFL.txt
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2020 The Anton Project Authors (https://github.com/googlefonts/AntonFont.git)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
AIVision/genai.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Róbert Malovec
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from .platforms import Platforms
|
|
24
|
+
from openai import OpenAI
|
|
25
|
+
import os
|
|
26
|
+
import base64
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AIPlatform:
|
|
30
|
+
"""Configuration class for AI platform settings."""
|
|
31
|
+
DEFAULT_IMG_DETAIL = "high"
|
|
32
|
+
|
|
33
|
+
def __init__(self, platform: Platforms = None, base_url: str = None,
|
|
34
|
+
api_key: str = None, model: str = None, image_detail: str = DEFAULT_IMG_DETAIL):
|
|
35
|
+
"""
|
|
36
|
+
Initialize AI platform configuration.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
platform: Platform enum value
|
|
40
|
+
base_url: Custom base URL (overrides platform default)
|
|
41
|
+
api_key: API key for authentication
|
|
42
|
+
model: Model name (overrides platform default)
|
|
43
|
+
image_detail: Image detail level for vision models
|
|
44
|
+
"""
|
|
45
|
+
self.platform = platform
|
|
46
|
+
self.base_url = base_url or (platform.value["default_base_url"] if platform else None)
|
|
47
|
+
self.model = model or (platform.value["default_model"] if platform else None)
|
|
48
|
+
self.detail = image_detail
|
|
49
|
+
self.api_key = api_key
|
|
50
|
+
self.supports_vision = platform.value.get("supports_vision", False) if platform else False
|
|
51
|
+
|
|
52
|
+
# Validate API key requirement
|
|
53
|
+
if platform and platform.value.get("api_key_required", False) and not api_key:
|
|
54
|
+
raise ValueError(f"{platform.name} requires an API key")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GenAI:
|
|
58
|
+
"""
|
|
59
|
+
GenAI class for interacting with multiple AI platforms using OpenAI-compatible API.
|
|
60
|
+
Supports Ollama, Perplexity, and is easily extensible for other providers.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
AUTOMATOR_INSTRUCTION = """
|
|
64
|
+
You are a response system for Robot Framework, specialized in test automation.
|
|
65
|
+
Your task is to evaluate an input instruction (assertion) against one or more provided images.
|
|
66
|
+
You must verify whether the assertion holds true based on the visual content of the images.
|
|
67
|
+
Make sure you observe images in every detail - all the logos, texts, titles, buttons, elements, inputs.
|
|
68
|
+
|
|
69
|
+
Your response must be strictly formatted like this:
|
|
70
|
+
|
|
71
|
+
RESULT: // PASS if assertion is verified, FAIL if not
|
|
72
|
+
EXPLANATION:
|
|
73
|
+
<brief explanation if TRUE, detailed explanation if FALSE>
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
When the assertion is TRUE:
|
|
77
|
+
Confirm the assertion and provide a brief explanation of why it was verified successfully.
|
|
78
|
+
|
|
79
|
+
When the assertion is FALSE:
|
|
80
|
+
Explain in detail what was wrong and why the assertion could not be verified.
|
|
81
|
+
|
|
82
|
+
Highlight any visual discrepancies, missing elements, or mismatches.
|
|
83
|
+
|
|
84
|
+
Example Inputs and Outputs:
|
|
85
|
+
|
|
86
|
+
Input Instruction: "The login button is visible and labeled 'Sign In'"
|
|
87
|
+
Provided Image: [screenshot of a login form]
|
|
88
|
+
|
|
89
|
+
Response when TRUE:
|
|
90
|
+
|
|
91
|
+
RESULT: pass
|
|
92
|
+
EXPLANATION:
|
|
93
|
+
1. The login button is clearly visible
|
|
94
|
+
2. The login button is labeled 'Sign In' as expected.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
Response when FALSE:
|
|
98
|
+
|
|
99
|
+
RESULT: fail
|
|
100
|
+
EXPLANATION:
|
|
101
|
+
1. The login button is either not visible or not labeled 'Sign In'.
|
|
102
|
+
2. The visible button is labeled 'Log In' instead.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
Ensure no other text is provided in the response.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, platform: Platforms = Platforms.Ollama, base_url: str = None,
|
|
109
|
+
api_key: str = None, model: str = None, image_detail: str = None,
|
|
110
|
+
simple_response: bool = True, initialize: bool = True,
|
|
111
|
+
system_prompt: str = AUTOMATOR_INSTRUCTION):
|
|
112
|
+
"""
|
|
113
|
+
Initialize GenAI instance.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
platform: AI platform to use (default: Ollama)
|
|
117
|
+
base_url: Custom base URL for API endpoint
|
|
118
|
+
api_key: API key for authentication
|
|
119
|
+
model: Model name to use
|
|
120
|
+
image_detail: Detail level for image processing
|
|
121
|
+
simple_response: Return simplified responses
|
|
122
|
+
initialize: Initialize client immediately
|
|
123
|
+
system_prompt: Main AI System prompt specifying Gen AI behavior
|
|
124
|
+
"""
|
|
125
|
+
self.client = None
|
|
126
|
+
self.simple_response = simple_response
|
|
127
|
+
self.system_prompt = system_prompt or self.AUTOMATOR_INSTRUCTION
|
|
128
|
+
|
|
129
|
+
# Set default API key for platforms that don't require real keys
|
|
130
|
+
if platform == Platforms.Ollama and not api_key:
|
|
131
|
+
api_key = "ollama" # Required by OpenAI client but ignored by Ollama
|
|
132
|
+
|
|
133
|
+
self.ai_platform = AIPlatform(
|
|
134
|
+
platform=platform,
|
|
135
|
+
base_url=base_url,
|
|
136
|
+
api_key=api_key,
|
|
137
|
+
model=model,
|
|
138
|
+
image_detail=image_detail
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if initialize:
|
|
142
|
+
self.initialize_genai(ai_platform=self.ai_platform)
|
|
143
|
+
|
|
144
|
+
def initialize_genai(self, ai_platform: AIPlatform = None):
|
|
145
|
+
"""
|
|
146
|
+
Initialize the OpenAI client with platform-specific configuration.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
ai_platform: AIPlatform configuration object
|
|
150
|
+
"""
|
|
151
|
+
if not ai_platform:
|
|
152
|
+
raise ValueError("AI platform not specified")
|
|
153
|
+
|
|
154
|
+
if not ai_platform.base_url:
|
|
155
|
+
raise ValueError("Base URL is required")
|
|
156
|
+
|
|
157
|
+
# Initialize OpenAI client with platform-specific settings
|
|
158
|
+
self.client = OpenAI(
|
|
159
|
+
base_url=ai_platform.base_url,
|
|
160
|
+
api_key=ai_platform.api_key or "default"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self.ai_platform = ai_platform
|
|
164
|
+
|
|
165
|
+
def generate_ai_response(self, instructions: str, image_paths: list):
|
|
166
|
+
"""
|
|
167
|
+
Generate AI response for test automation assertions with images.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
instructions: Test assertion instructions
|
|
171
|
+
image_paths: List of image file paths to analyze
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
AI-generated response
|
|
175
|
+
"""
|
|
176
|
+
prompt = self._prepare_prompt(instructions, image_paths)
|
|
177
|
+
return self.chat_completion(prompt)
|
|
178
|
+
|
|
179
|
+
def chat_completion(self, messages):
|
|
180
|
+
"""
|
|
181
|
+
Execute chat completion request.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
messages: List of message dictionaries in OpenAI format
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Response content (simplified or full based on simple_response setting)
|
|
188
|
+
"""
|
|
189
|
+
if not self.client:
|
|
190
|
+
raise RuntimeError("GenAI client not initialized. Call initialize_genai() first.")
|
|
191
|
+
|
|
192
|
+
# Convert messages format if needed (handle custom image format)
|
|
193
|
+
formatted_messages = self._format_messages_for_openai(messages)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
response = self.client.chat.completions.create(
|
|
197
|
+
model=self.ai_platform.model,
|
|
198
|
+
messages=formatted_messages
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if self.simple_response:
|
|
202
|
+
return response.choices[0].message.content
|
|
203
|
+
else:
|
|
204
|
+
return response
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
raise Exception(f"Error during chat completion: {str(e)}")
|
|
208
|
+
|
|
209
|
+
def _prepare_prompt(self, instruction: str, image_paths: list = None):
|
|
210
|
+
"""
|
|
211
|
+
Prepare prompt with instructions and images for test automation.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
instruction: Test instruction/assertion
|
|
215
|
+
image_paths: List of image file paths
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Formatted prompt as list of messages
|
|
219
|
+
"""
|
|
220
|
+
content = [
|
|
221
|
+
{
|
|
222
|
+
"type": "text",
|
|
223
|
+
"text": self.system_prompt
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"type": "text",
|
|
227
|
+
"text": instruction
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
# Add images if vision is supported
|
|
232
|
+
if image_paths and self.ai_platform.supports_vision:
|
|
233
|
+
for img_path in image_paths:
|
|
234
|
+
if not os.path.isfile(img_path):
|
|
235
|
+
raise FileNotFoundError(f"Image not found: {img_path}")
|
|
236
|
+
|
|
237
|
+
content.append({
|
|
238
|
+
"type": "image",
|
|
239
|
+
"image_path": img_path
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
return [{"role": "user", "content": content}]
|
|
243
|
+
|
|
244
|
+
def _format_messages_for_openai(self, messages):
|
|
245
|
+
"""
|
|
246
|
+
Convert custom message format to OpenAI-compatible format.
|
|
247
|
+
Handles image paths by converting them to base64 data URIs.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
messages: List of messages in custom format
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of messages in OpenAI format
|
|
254
|
+
"""
|
|
255
|
+
formatted_messages = []
|
|
256
|
+
|
|
257
|
+
for message in messages:
|
|
258
|
+
formatted_content = []
|
|
259
|
+
|
|
260
|
+
for item in message.get("content", []):
|
|
261
|
+
if item.get("type") == "image":
|
|
262
|
+
# Convert image file to base64 data URI
|
|
263
|
+
image_path = item.get("image_path")
|
|
264
|
+
if image_path and self.ai_platform.supports_vision:
|
|
265
|
+
image_data = self._encode_image_to_base64(image_path)
|
|
266
|
+
formatted_content.append({
|
|
267
|
+
"type": "image_url",
|
|
268
|
+
"image_url": {
|
|
269
|
+
"url": image_data,
|
|
270
|
+
"detail": self.ai_platform.detail
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
elif item.get("type") == "text":
|
|
274
|
+
formatted_content.append({
|
|
275
|
+
"type": "text",
|
|
276
|
+
"text": item.get("text", "")
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
formatted_messages.append({
|
|
280
|
+
"role": message.get("role"),
|
|
281
|
+
"content": formatted_content
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
return formatted_messages
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _encode_image_to_base64(image_path: str) -> str:
|
|
288
|
+
"""
|
|
289
|
+
Encode image file to base64 data URI.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
image_path: Path to image file
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Base64-encoded data URI string
|
|
296
|
+
"""
|
|
297
|
+
with open(image_path, "rb") as image_file:
|
|
298
|
+
image_data = base64.b64encode(image_file.read()).decode('utf-8')
|
|
299
|
+
|
|
300
|
+
# Detect image format from file extension
|
|
301
|
+
ext = os.path.splitext(image_path)[1].lower()
|
|
302
|
+
mime_types = {
|
|
303
|
+
'.png': 'image/png',
|
|
304
|
+
'.jpg': 'image/jpeg',
|
|
305
|
+
'.jpeg': 'image/jpeg',
|
|
306
|
+
'.gif': 'image/gif',
|
|
307
|
+
'.webp': 'image/webp'
|
|
308
|
+
}
|
|
309
|
+
mime_type = mime_types.get(ext, 'image/png')
|
|
310
|
+
|
|
311
|
+
return f"data:{mime_type};base64,{image_data}"
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def extract_result_and_explanation_from_response(response: str):
|
|
315
|
+
"""
|
|
316
|
+
Extract RESULT and EXPLANATION from formatted response.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
response: AI response string
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Tuple of (result, explanation)
|
|
323
|
+
"""
|
|
324
|
+
parts = response.split("RESULT:", 1)
|
|
325
|
+
if len(parts) < 2:
|
|
326
|
+
return "fail", response
|
|
327
|
+
|
|
328
|
+
result_and_explanation = parts[1].strip()
|
|
329
|
+
|
|
330
|
+
parts = result_and_explanation.split("EXPLANATION:", 1)
|
|
331
|
+
if len(parts) < 2:
|
|
332
|
+
return parts[0].strip(), response
|
|
333
|
+
|
|
334
|
+
result = parts[0].strip()
|
|
335
|
+
explanation = parts[1].strip()
|
|
336
|
+
|
|
337
|
+
return result, explanation
|
AIVision/library.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Róbert Malovec
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from .genai import GenAI
|
|
24
|
+
from .genai import Platforms
|
|
25
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
26
|
+
from robot.api.deco import keyword
|
|
27
|
+
from robot.api import logger
|
|
28
|
+
from robot.libraries.BuiltIn import BuiltIn, RobotNotRunningError
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
GenAI Testing library module for Robot Framework
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_rf_output_dir():
|
|
38
|
+
"""Returns Robot Framework output directory path"""
|
|
39
|
+
try:
|
|
40
|
+
output_dir = BuiltIn().get_variable_value("${OUTPUT_DIR}")
|
|
41
|
+
except RobotNotRunningError:
|
|
42
|
+
output_dir = os.getcwd()
|
|
43
|
+
|
|
44
|
+
return output_dir
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AIVision:
|
|
48
|
+
"""
|
|
49
|
+
AI Vision Library module for Robot Framework
|
|
50
|
+
|
|
51
|
+
This RF library provides GenAI enabled front-end, UI and visual templates testing capabilities
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
ROBOT_LIBRARY_SCOPE = "GLOBAL"
|
|
55
|
+
FONT = os.path.join(
|
|
56
|
+
os.path.dirname(os.path.realpath(__file__)), "font", "Anton-Regular.ttf"
|
|
57
|
+
)
|
|
58
|
+
OUTPUT_DIR = _get_rf_output_dir()
|
|
59
|
+
|
|
60
|
+
def __init__(self, base_url: str = None, api_key: str = None, platform: Platforms = Platforms.Ollama,
|
|
61
|
+
model: str = None, image_detail: str = None, simple_response: bool = True,
|
|
62
|
+
initialize: bool = True, system_prompt: str = None):
|
|
63
|
+
|
|
64
|
+
self.genai = GenAI(base_url=base_url, api_key=api_key, platform=platform,
|
|
65
|
+
model=model, image_detail=image_detail,
|
|
66
|
+
simple_response=simple_response, initialize=initialize, system_prompt=system_prompt)
|
|
67
|
+
|
|
68
|
+
@keyword
|
|
69
|
+
def verify_that(self, screenshot_paths, instructions):
|
|
70
|
+
"""Verifies that the image matches the instructions
|
|
71
|
+
|
|
72
|
+
Input parameters:
|
|
73
|
+
|
|
74
|
+
``image_path``: (required) Path(s) to the image. Can be a single path or a list of paths
|
|
75
|
+
|
|
76
|
+
``instructions``: (required) Instructions to be verified
|
|
77
|
+
|
|
78
|
+
*Examples*:
|
|
79
|
+
|
|
80
|
+
| Verify That | /path/to/image.png | Contains green logo in top right corner |
|
|
81
|
+
|
|
82
|
+
| @{img_paths} = | Create List | /path/to/image1.png | /path/to/image2.png |
|
|
83
|
+
| Verify That | ${img_paths} | First image contains logo as referenced on 2nd image. |
|
|
84
|
+
"""
|
|
85
|
+
screenshot_paths = [screenshot_paths] if isinstance(screenshot_paths, str) else screenshot_paths
|
|
86
|
+
response = self.genai.generate_ai_response(instructions=f"Verify that: {instructions}", image_paths=screenshot_paths)
|
|
87
|
+
logger.debug(response)
|
|
88
|
+
|
|
89
|
+
self._assert_result(response)
|
|
90
|
+
|
|
91
|
+
@keyword
|
|
92
|
+
def verify_screenshot_matches_look_and_feel_template(self, screenshot_path, template_path,
|
|
93
|
+
override_instructions: str = None,
|
|
94
|
+
create_combined_image: bool = True):
|
|
95
|
+
"""Verifies that the screenshot matches the look and feel template
|
|
96
|
+
|
|
97
|
+
Input parameters:
|
|
98
|
+
|
|
99
|
+
``screenshot_path``: (required) Path to the screenshot image
|
|
100
|
+
|
|
101
|
+
``template_path``: (required) Path to the template image
|
|
102
|
+
|
|
103
|
+
``override_instructions``: (optional) If specified, it will override the built-in assertion instructions
|
|
104
|
+
|
|
105
|
+
``create_combined_image``: (optional) default is _True_. If _True_, combined image will be created and saved
|
|
106
|
+
|
|
107
|
+
_Return Value_ is the path of the saved image
|
|
108
|
+
|
|
109
|
+
*Examples*:
|
|
110
|
+
| Verify Screenshot Matches Look And Feel Template | /path/to/screenshot.png | /path/to/template.png |
|
|
111
|
+
| Verify Screenshot Matches Look And Feel Template | /path/to/screenshot.png | /path/to/template.png | override_instructions=Custom instructions |
|
|
112
|
+
"""
|
|
113
|
+
if create_combined_image:
|
|
114
|
+
try:
|
|
115
|
+
self.combine_images_on_paths_side_by_side(screenshot_path, template_path, "Actual", "Expected",
|
|
116
|
+
save=True)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.warn(f"Could not create combined image: {e}")
|
|
119
|
+
|
|
120
|
+
instructions = """First image is showing actual application view.
|
|
121
|
+
Second image is reference design template.
|
|
122
|
+
Verify screenshot matches look and feel template. Pay attention to details, design is important.
|
|
123
|
+
Make sure to check also all the visible logos, titles, labels, spelling, texts, links, menus, banners
|
|
124
|
+
and any available graphics. Always doublecheck the reference image in case you think some
|
|
125
|
+
text, label, logo or element is overlapping or containing typo.
|
|
126
|
+
"""
|
|
127
|
+
if override_instructions:
|
|
128
|
+
instructions = override_instructions
|
|
129
|
+
response = self.genai.generate_ai_response(
|
|
130
|
+
instructions=instructions,
|
|
131
|
+
image_paths=[screenshot_path, template_path])
|
|
132
|
+
|
|
133
|
+
self._assert_result(response)
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
@keyword
|
|
137
|
+
def open_image(image_path, mode="RGB"):
|
|
138
|
+
"""Opens image from provided path
|
|
139
|
+
|
|
140
|
+
Input parameters:
|
|
141
|
+
|
|
142
|
+
``image_path``: (required) Path to the image
|
|
143
|
+
|
|
144
|
+
``mode``: (optional) default is _RGB_.
|
|
145
|
+
Defines type and depth of a pixel to which the opened image will be converted.
|
|
146
|
+
Supported modes can be seen
|
|
147
|
+
[https://pillow.readthedocs.io/en/3.0.x/handbook/concepts.html#modes|here].
|
|
148
|
+
|
|
149
|
+
_Return value_ is the PIL Image object
|
|
150
|
+
|
|
151
|
+
*Example*:
|
|
152
|
+
| ${image} = | Open Image | /path/to/image.png |
|
|
153
|
+
| ${image} = | Open Image | /path/to/image.png | RGBA |
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
image = Image.open(image_path)
|
|
158
|
+
logger.debug(f"Image '{image_path}' was opened successfully")
|
|
159
|
+
except Exception as err:
|
|
160
|
+
raise AssertionError(
|
|
161
|
+
f"Could not open image on provided path:\n{type(err).__name__}: {err}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if image.mode != mode:
|
|
165
|
+
logger.debug(
|
|
166
|
+
f"Image is in mode '{image.mode}' but desired is '{mode}'. Starting conversion."
|
|
167
|
+
)
|
|
168
|
+
try:
|
|
169
|
+
image = image.convert(mode=mode)
|
|
170
|
+
logger.debug(f"Image successfully converted to mode '{mode}'")
|
|
171
|
+
except Exception as err:
|
|
172
|
+
raise AssertionError(
|
|
173
|
+
f"Could not convert image to provided mode:\n{type(err).__name__}: {err}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return image
|
|
177
|
+
|
|
178
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
179
|
+
@keyword
|
|
180
|
+
def save_image(
|
|
181
|
+
self,
|
|
182
|
+
image,
|
|
183
|
+
image_name=None,
|
|
184
|
+
image_format=None,
|
|
185
|
+
watermark=None,
|
|
186
|
+
image_path=OUTPUT_DIR,
|
|
187
|
+
):
|
|
188
|
+
"""Saves image to provided path and name
|
|
189
|
+
|
|
190
|
+
Input parameters:
|
|
191
|
+
|
|
192
|
+
``image``: (required) PIL image object to save
|
|
193
|
+
|
|
194
|
+
``image_name``: (optional) Name of the image.
|
|
195
|
+
If empty image name will be auto-generated
|
|
196
|
+
|
|
197
|
+
``image_format``: (optional) If not set the image format will be determined from the _image_name_ extension
|
|
198
|
+
if specified there
|
|
199
|
+
|
|
200
|
+
``watermark``: (optional) If the specified image will be watermarked with the specified string in top left corner
|
|
201
|
+
|
|
202
|
+
``image_path``: (optional) Path to image to save.
|
|
203
|
+
If not specified images are being stored to the Robot Framework output directory
|
|
204
|
+
|
|
205
|
+
_Return Value_: Path of the saved image
|
|
206
|
+
|
|
207
|
+
*Examples*:
|
|
208
|
+
| Save Image | ${image}| my_image.png |
|
|
209
|
+
| Save Image | ${image}| my_image | png |
|
|
210
|
+
| Save Image | ${image}| my_image.png | watermark=My Label |
|
|
211
|
+
| Save Image | ${image}| my_image.png | image_path=/path/to/my/image/directory |
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
if not image_name:
|
|
215
|
+
if image_format:
|
|
216
|
+
image_name = self.generate_image_name(extension=image_format)
|
|
217
|
+
else:
|
|
218
|
+
image_name = self.generate_image_name()
|
|
219
|
+
|
|
220
|
+
dest = os.path.join(image_path, image_name)
|
|
221
|
+
|
|
222
|
+
if image_format:
|
|
223
|
+
dest = os.path.join(dest, ".", image_format.lower())
|
|
224
|
+
|
|
225
|
+
logger.debug(f"Image will be saved to '{dest}'")
|
|
226
|
+
|
|
227
|
+
if watermark:
|
|
228
|
+
logger.debug(f"Adding watermark '{watermark}' to image")
|
|
229
|
+
image = self.add_watermark_to_image(image, watermark)
|
|
230
|
+
|
|
231
|
+
image.save(dest)
|
|
232
|
+
|
|
233
|
+
except Exception as err:
|
|
234
|
+
raise AssertionError(f"Could not save image:\n{type(err).__name__}: {err}")
|
|
235
|
+
|
|
236
|
+
logger.info(
|
|
237
|
+
f"<img width='800' src='{os.path.relpath(dest, image_path)}'/>", html=True
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return dest
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
@keyword
|
|
244
|
+
def generate_image_name(prefix="Snap", extension="png"):
|
|
245
|
+
"""Generates unique image name with the specified prefix and optional image extension
|
|
246
|
+
|
|
247
|
+
Input parameters:
|
|
248
|
+
|
|
249
|
+
``prefix``: (optional) default is "Image"
|
|
250
|
+
|
|
251
|
+
``extension``: (optional) default is _png_
|
|
252
|
+
|
|
253
|
+
_Return Value_ is generated string representing unique image name
|
|
254
|
+
|
|
255
|
+
*Examples*:
|
|
256
|
+
| ${img_name} = | Generate Image Name |
|
|
257
|
+
| ${img_name} = | Generate Image Name | My-Image |
|
|
258
|
+
| ${img_name} = | Generate Image Name | My-Image | jpg |
|
|
259
|
+
"""
|
|
260
|
+
if not prefix:
|
|
261
|
+
prefix = ""
|
|
262
|
+
name_template = "%s%s"
|
|
263
|
+
else:
|
|
264
|
+
name_template = "%s-%s"
|
|
265
|
+
|
|
266
|
+
image_name = name_template % (
|
|
267
|
+
prefix,
|
|
268
|
+
datetime.now().strftime("%m-%d-%Y_%H-%M-%S-%f")[:-3],
|
|
269
|
+
)
|
|
270
|
+
if extension:
|
|
271
|
+
image_name = f"{image_name}.{extension.lower()}"
|
|
272
|
+
|
|
273
|
+
logger.debug(f"Generated image name is: {image_name}")
|
|
274
|
+
|
|
275
|
+
return image_name
|
|
276
|
+
|
|
277
|
+
@keyword
|
|
278
|
+
def combine_images_on_paths_side_by_side(self, image_path1, image_path2, watermark1=None, watermark2=None,
|
|
279
|
+
mode="RGB", save=True):
|
|
280
|
+
"""Combines two images specified by file path to one big image side-by-side
|
|
281
|
+
|
|
282
|
+
Input parameters:
|
|
283
|
+
|
|
284
|
+
``image_path1``: (required) Path to the first image
|
|
285
|
+
|
|
286
|
+
``image_path2``: (required) Path to the second image
|
|
287
|
+
|
|
288
|
+
``watermark1``: (optional) If specified image1 will be watermarked with the specified string in top left corner
|
|
289
|
+
|
|
290
|
+
``watermark2``: (optional) If specified image2 will be watermarked with the specified string in top left corner
|
|
291
|
+
|
|
292
|
+
``mode``: (optional) default is _RGB_.
|
|
293
|
+
|
|
294
|
+
_Return Value_ is combined image as PIL Image format
|
|
295
|
+
|
|
296
|
+
*Examples*:
|
|
297
|
+
| ${image} = | Combine Images On Paths Side By Side | /path/to/image1.png | /path/to/image2.png |
|
|
298
|
+
| ${image} = | Combine Images On Paths Side By Side | /path/to/image1.png | /path/to/image2.png | Expected | Actual |
|
|
299
|
+
|
|
300
|
+
"""
|
|
301
|
+
img1 = self.open_image(image_path1, mode=mode)
|
|
302
|
+
img2 = self.open_image(image_path2, mode=mode)
|
|
303
|
+
|
|
304
|
+
combined_img = self.combine_images_side_by_side(img1, img2, watermark1=watermark1, watermark2=watermark2,
|
|
305
|
+
mode=mode)
|
|
306
|
+
|
|
307
|
+
if save:
|
|
308
|
+
self.save_image(combined_img)
|
|
309
|
+
|
|
310
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
311
|
+
@keyword
|
|
312
|
+
def combine_images_side_by_side(
|
|
313
|
+
self, image1, image2, watermark1=None, watermark2=None, mode="RGB"
|
|
314
|
+
):
|
|
315
|
+
"""Combines two images to one big image side-by-side
|
|
316
|
+
|
|
317
|
+
Input parameters:
|
|
318
|
+
|
|
319
|
+
``image1``: (required) Image one (PIL Image object) to combine
|
|
320
|
+
|
|
321
|
+
``image2``: (required) Image two (PIL Image object) to combine
|
|
322
|
+
|
|
323
|
+
``watermark1``: (optional) If specified image1 will be watermarked with the specified string in top left corner
|
|
324
|
+
|
|
325
|
+
``watermark2``: (optional) If specified image2 will be watermarked with the specified string in top left corner
|
|
326
|
+
|
|
327
|
+
``mode``: (optional) default is _RGB_.
|
|
328
|
+
Defines type and depth of a pixel which will be used for watermark layer.
|
|
329
|
+
You do not need typically change this value.
|
|
330
|
+
Supported modes can be seen
|
|
331
|
+
[https://pillow.readthedocs.io/en/3.0.x/handbook/concepts.html#modes|here].
|
|
332
|
+
|
|
333
|
+
_Return Value_ is combined image as PIL Image format
|
|
334
|
+
|
|
335
|
+
*Examples*:
|
|
336
|
+
| ${image} = | Combine Images Side By Side | ${image1} | ${image2} |
|
|
337
|
+
| ${image} = | Combine Images Side By Side | ${image1} | ${image2} |RGBA |
|
|
338
|
+
"""
|
|
339
|
+
try:
|
|
340
|
+
# Create empty image for both images to fit
|
|
341
|
+
combined_image = Image.new(
|
|
342
|
+
mode,
|
|
343
|
+
(
|
|
344
|
+
image1.size[0] + image2.size[0] + 1,
|
|
345
|
+
max(image1.size[1], image2.size[1]),
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if watermark1:
|
|
350
|
+
logger.debug(f"Adding watermark '{watermark1}' to image1")
|
|
351
|
+
image1 = self.add_watermark_to_image(image1, watermark1)
|
|
352
|
+
|
|
353
|
+
if watermark2:
|
|
354
|
+
logger.debug(f"Adding watermark '{watermark2}' to image2")
|
|
355
|
+
image2 = self.add_watermark_to_image(image2, watermark2)
|
|
356
|
+
|
|
357
|
+
# Concatenate both images to one big image
|
|
358
|
+
combined_image.paste(image1, (0, 0))
|
|
359
|
+
combined_image.paste(image2, (image1.size[0] + 1, 0))
|
|
360
|
+
|
|
361
|
+
except Exception as err:
|
|
362
|
+
raise AssertionError(
|
|
363
|
+
f"Could not create combined image:\n{type(err).__name__}: {err}"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return combined_image
|
|
367
|
+
|
|
368
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
369
|
+
@keyword
|
|
370
|
+
def add_watermark_to_image(
|
|
371
|
+
self, image, text, color="red", text_size=50, text_position=(0, 0), mode="RGB"
|
|
372
|
+
):
|
|
373
|
+
"""Adds watermark text to the image
|
|
374
|
+
|
|
375
|
+
Input parameters:
|
|
376
|
+
|
|
377
|
+
``image``: (required) PIL image object to add watermark to
|
|
378
|
+
|
|
379
|
+
``text``: (required) Text string which will be added to the image
|
|
380
|
+
|
|
381
|
+
``color``: (optional) default is _red_.
|
|
382
|
+
Text color of the watermark
|
|
383
|
+
|
|
384
|
+
``text_size``: (optional) default is _50_.
|
|
385
|
+
Text size of the watermark
|
|
386
|
+
|
|
387
|
+
``text_position``: (optional) default is _(0, 0)_.
|
|
388
|
+
Represents X,Y coordinates in the image where to add watermark
|
|
389
|
+
|
|
390
|
+
``mode``: (optional) default is _RGB_.
|
|
391
|
+
Defines type and depth of a pixel which will be used for watermark layer.
|
|
392
|
+
You do not need typically change this value.
|
|
393
|
+
Supported modes can be seen
|
|
394
|
+
[https://pillow.readthedocs.io/en/3.0.x/handbook/concepts.html#modes|here].
|
|
395
|
+
|
|
396
|
+
_Return Value_ represents PIL Image object
|
|
397
|
+
|
|
398
|
+
*Examples*:
|
|
399
|
+
| ${w_image} = | Add Watermark To Image | ${image} | Original |
|
|
400
|
+
| ${w_image} = | Add Watermark To Image | ${image} | Original | blue |
|
|
401
|
+
| ${w_image} = | Add Watermark To Image | ${image} | text=Original | color=blue |
|
|
402
|
+
"""
|
|
403
|
+
font_path = self.FONT
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
font = ImageFont.truetype(font_path, text_size)
|
|
407
|
+
except Exception as err:
|
|
408
|
+
raise AssertionError(
|
|
409
|
+
f"Could not set watermark font:\n{type(err).__name__}: {err}"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
# Create watermark drawing canvas object
|
|
414
|
+
watermark = Image.new(mode, image.size)
|
|
415
|
+
draw = ImageDraw.ImageDraw(watermark, mode)
|
|
416
|
+
|
|
417
|
+
# Create semi-transparent watermark text
|
|
418
|
+
draw.text(text_position, text, fill=color, font=font)
|
|
419
|
+
mask = watermark.convert("L").point(lambda x: min(x, 100))
|
|
420
|
+
watermark.putalpha(mask)
|
|
421
|
+
|
|
422
|
+
# Create new image with watermark
|
|
423
|
+
w_image = image.copy()
|
|
424
|
+
w_image.paste(watermark, None, watermark)
|
|
425
|
+
except Exception as err:
|
|
426
|
+
raise AssertionError(
|
|
427
|
+
f"Could not create watermark:\n{type(err).__name__}: {err}"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
return w_image
|
|
431
|
+
|
|
432
|
+
def _assert_result(self, response):
|
|
433
|
+
result, explanation = self.genai.extract_result_and_explanation_from_response(response)
|
|
434
|
+
if result and result.lower() == "pass":
|
|
435
|
+
logger.info(f"Verification passed:\n{explanation}")
|
|
436
|
+
else:
|
|
437
|
+
raise AssertionError(f"Verification failed:\n{explanation}")
|
AIVision/platforms.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Róbert Malovec
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
from enum import Enum
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Platforms(Enum):
|
|
27
|
+
"""Enum defining supported AI platforms with their default configurations."""
|
|
28
|
+
Ollama = {
|
|
29
|
+
"default_model": "qwen3-coder:480b-cloud",
|
|
30
|
+
"default_base_url": "http://localhost:11434/v1",
|
|
31
|
+
"api_key_required": False,
|
|
32
|
+
"supports_vision": True
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
DockerModel = {
|
|
36
|
+
"default_model": "ai/qwen3-vl:8B-Q8_K_XL",
|
|
37
|
+
"default_base_url": "http://localhost:12434/engines/v1",
|
|
38
|
+
"api_key_required": False,
|
|
39
|
+
"supports_vision": True
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
OpenAI = {
|
|
43
|
+
"default_model": "gpt-5.2",
|
|
44
|
+
"default_base_url": "https://api.openai.com/v1",
|
|
45
|
+
"api_key_required": True,
|
|
46
|
+
"supports_vision": True
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Perplexity = {
|
|
50
|
+
"default_model": "sonar-pro",
|
|
51
|
+
"default_base_url": "https://api.perplexity.ai",
|
|
52
|
+
"api_key_required": True,
|
|
53
|
+
"supports_vision": True
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Gemini = {
|
|
57
|
+
"default_model": "gemini-2.5-flash",
|
|
58
|
+
"default_base_url": "https://generativelanguage.googleapis.com/v1beta/openai/",
|
|
59
|
+
"api_key_required": True,
|
|
60
|
+
"supports_vision": True
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Manual = {
|
|
64
|
+
"default_model": None,
|
|
65
|
+
"default_base_url": None,
|
|
66
|
+
"api_key_required": True,
|
|
67
|
+
"supports_vision": True
|
|
68
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotframework_aivision
|
|
3
|
+
Version: 0.2.0a1
|
|
4
|
+
Summary: AI Vision library for Robot Framework
|
|
5
|
+
Home-page: https://github.com/robco/robotframework-aivision.git
|
|
6
|
+
Author: Róbert Malovec
|
|
7
|
+
Author-email: robert@malovec.sk
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: LICENSE.txt
|
|
13
|
+
Requires-Dist: robotframework
|
|
14
|
+
Requires-Dist: pillow
|
|
15
|
+
Requires-Dist: openai
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: license
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
[](https://www.buymeacoffee.com/robco)
|
|
27
|
+
# Robot Framework AI Vision Library
|
|
28
|
+
|
|
29
|
+
AI VIsion Library for Robot Framework that verifies UI/screenshots (including template “look & feel”) by sending instructions plus one or more images to an OpenAI-compatible API (Ollama, OpenAI, Perplexity, Gemini, etc.).
|
|
30
|
+
|
|
31
|
+
The main keyword (`Verify That`) expects the model to return a strict `RESULT:` / `EXPLANATION:` format and will fail the test if the result is not `pass`.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Visual assertions on one or more screenshots using natural-language instructions.
|
|
36
|
+
- Template comparison keyword to validate “actual vs expected” look & feel (optionally creates a side-by-side image).
|
|
37
|
+
- Image utilities built on Pillow: open/convert, watermark, combine images, auto-generate names, save into Robot Framework output directory.
|
|
38
|
+
- Works with multiple providers via the `openai` Python client and OpenAI-compatible endpoints (`base_url`).
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
**Install from PyPI (once published):**
|
|
43
|
+
```bash
|
|
44
|
+
pip install -U robotframework-aivision
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Runtime dependencies include Robot Framework, Pillow, and the `openai` Python client.
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
Import the library in Robot Framework and choose a provider using `platform` plus optional overrides (`base_url`, `api_key`, `model`, `image_detail`).
|
|
52
|
+
|
|
53
|
+
### Robot Framework import examples
|
|
54
|
+
|
|
55
|
+
**Default (Ollama-like local setup):**
|
|
56
|
+
```robotframework
|
|
57
|
+
*** Settings ***
|
|
58
|
+
Library AIVision
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**OpenAI (API key required):**
|
|
62
|
+
```robotframework
|
|
63
|
+
*** Settings ***
|
|
64
|
+
Library AIVision
|
|
65
|
+
... platform=OpenAI
|
|
66
|
+
... api_key=%{OPENAI_API_KEY}
|
|
67
|
+
... model=gpt-5.2
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Perplexity:**
|
|
71
|
+
```robotframework
|
|
72
|
+
*** Settings ***
|
|
73
|
+
Library AIVision
|
|
74
|
+
... platform=Perplexity
|
|
75
|
+
... api_key=%{PPLX_API_KEY}
|
|
76
|
+
... model=sonar-pro
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Gemini (OpenAI-compatible endpoint):**
|
|
80
|
+
```robotframework
|
|
81
|
+
*** Settings ***
|
|
82
|
+
Library AIVision
|
|
83
|
+
... platform=Gemini
|
|
84
|
+
... api_key=%{GEMINI_API_KEY}
|
|
85
|
+
... model=gemini-2.5-flash
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Supported platforms (defaults)
|
|
89
|
+
|
|
90
|
+
The library defines these platform presets (model and `base_url`) which you can override via import arguments.
|
|
91
|
+
|
|
92
|
+
| Platform | Default `base_url` | Default model | API key |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| Ollama | `http://localhost:11434/v1` | `qwen3-coder:480b-cloud` | Not required |
|
|
95
|
+
| DockerModel | `http://localhost:12434/engines/v1` | `ai/qwen3-vl:8B-Q8_K_XL` | Not required. |
|
|
96
|
+
| OpenAI | `https://api.openai.com/v1` | `gpt-5.2` | Required. |
|
|
97
|
+
| Perplexity | `https://api.perplexity.ai` | `sonar-pro` | Required. |
|
|
98
|
+
| Gemini | `https://generativelanguage.googleapis.com/v1beta/openai/` | `gemini-2.5-flash` | Required. |
|
|
99
|
+
| Manual | `None` | `None` | Required. |
|
|
100
|
+
|
|
101
|
+
## Keywords
|
|
102
|
+
|
|
103
|
+
All keywords below are implemented in `AIVision` and are available after importing the library.
|
|
104
|
+
|
|
105
|
+
| Keyword | Purpose |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `Verify That` | Send one or more screenshots + instructions to the model, parse the `RESULT` and raise `AssertionError` on failure. |
|
|
108
|
+
| `Verify Screenshot Matches Look And Feel Template` | Compare a screenshot against a reference template with a built-in instruction set; optional combined image creation. |
|
|
109
|
+
| `Open Image` | Open an image (and optionally convert mode, default `RGB`). |
|
|
110
|
+
| `Save Image` | Save a PIL image to a path (defaults to RF output directory) with optional watermark. |
|
|
111
|
+
| `Generate Image Name` | Create a unique timestamp-based filename with prefix/extension. |
|
|
112
|
+
| `Combine Images On Paths Side By Side` | Combine two image files side-by-side (optionally watermark) and optionally save. |
|
|
113
|
+
| `Combine Images Side By Side` | Combine two in-memory PIL images side-by-side (optionally watermark). |
|
|
114
|
+
| `Add Watermark To Image` | Add watermark text using the included font file. |
|
|
115
|
+
|
|
116
|
+
## Usage examples
|
|
117
|
+
|
|
118
|
+
### Simple visual assertion
|
|
119
|
+
|
|
120
|
+
```robotframework
|
|
121
|
+
*** Settings ***
|
|
122
|
+
Library AIVision platform=Ollama
|
|
123
|
+
|
|
124
|
+
*** Test Cases ***
|
|
125
|
+
Login button is correct
|
|
126
|
+
Verify That ${CURDIR}/screens/login.png Login button is visible and labeled as 'Sign In'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Compare screenshot to design template
|
|
130
|
+
|
|
131
|
+
```robotframework
|
|
132
|
+
*** Settings ***
|
|
133
|
+
Library AIVision
|
|
134
|
+
|
|
135
|
+
*** Test Cases ***
|
|
136
|
+
Home page matches template
|
|
137
|
+
Verify Screenshot Matches Look And Feel Template
|
|
138
|
+
... ${CURDIR}/screens/home_actual.png
|
|
139
|
+
... ${CURDIR}/templates/home_expected.png
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Override template instructions
|
|
143
|
+
|
|
144
|
+
```robotframework
|
|
145
|
+
*** Settings ***
|
|
146
|
+
Library AIVision
|
|
147
|
+
|
|
148
|
+
*** Test Cases ***
|
|
149
|
+
Home page matches template - custom rules
|
|
150
|
+
Verify Screenshot Matches Look And Feel Template
|
|
151
|
+
... ${CURDIR}/screens/home_actual.png
|
|
152
|
+
... ${CURDIR}/templates/home_expected.png
|
|
153
|
+
... override_instructions=Verify layout, spacing, typography, and brand colors match the template exactly.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
Version history
|
|
158
|
+
----------------
|
|
159
|
+
|
|
160
|
+
0.2.0a1, 2026-02-02 -- Alpha1 version
|
|
161
|
+
0.2.0, 2026-01-29 -- AI System Prompt is configurable
|
|
162
|
+
0.1.0, 2025-12-19 -- Additional GenAI Providers added
|
|
163
|
+
0.0.1, 2024-05-11 -- Initial version
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
AIVision/__init__.py,sha256=Dp_ukmv2lgMOTdS-8xzUokr4IfC_aB91cGPfrinbHUA,1224
|
|
2
|
+
AIVision/genai.py,sha256=Na_EYVSGkir_1xqdpJocG5MVZUw9tSc9CV0JBEwxdzY,11678
|
|
3
|
+
AIVision/library.py,sha256=MZ-JDEC6rsuUuVscIijDtJcYgiiZEARdI94b_qLQ8Wc,16952
|
|
4
|
+
AIVision/platforms.py,sha256=vuozdhmHrhLlM85z4mHTlCiY2STvU_cgDyc0XD441vs,2388
|
|
5
|
+
AIVision/font/Anton-Regular.ttf,sha256=wkY8RDEDa8ak_mppboCdPAo9E3Md1h08n4wD9GYHpjE,161212
|
|
6
|
+
AIVision/font/OFL.txt,sha256=7mfm7iJ5C3kp8aN2nKKAHVZcZLWpCWlCwa31WW3pyeQ,4484
|
|
7
|
+
robotframework_aivision-0.2.0a1.dist-info/licenses/LICENSE,sha256=GnlimnQtg17QJ712UYWtmfPQFsme_q4cUxAO79yvkZ0,1072
|
|
8
|
+
robotframework_aivision-0.2.0a1.dist-info/licenses/LICENSE.txt,sha256=GnlimnQtg17QJ712UYWtmfPQFsme_q4cUxAO79yvkZ0,1072
|
|
9
|
+
robotframework_aivision-0.2.0a1.dist-info/METADATA,sha256=h0OhQJw3O_xFkLPf4IipgujylcwZSgXN-MvVydsAw4E,5529
|
|
10
|
+
robotframework_aivision-0.2.0a1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
robotframework_aivision-0.2.0a1.dist-info/top_level.txt,sha256=Z-P4V4dmnDuQoeciyqyQzGjkPLxcj924T4zKr5fNEuk,9
|
|
12
|
+
robotframework_aivision-0.2.0a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Róbert Malovec
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Róbert Malovec
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AIVision
|