robotframework-gemini 0.2.0__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.
- robotframework_gemini/__init__.py +21 -0
- robotframework_gemini/browser_helpers.py +50 -0
- robotframework_gemini/client.py +174 -0
- robotframework_gemini/legacy_env.py +37 -0
- robotframework_gemini/library.py +221 -0
- robotframework_gemini/prompt_build.py +71 -0
- robotframework_gemini/response_parse.py +33 -0
- robotframework_gemini-0.2.0.dist-info/METADATA +171 -0
- robotframework_gemini-0.2.0.dist-info/RECORD +12 -0
- robotframework_gemini-0.2.0.dist-info/WHEEL +4 -0
- robotframework_gemini-0.2.0.dist-info/entry_points.txt +2 -0
- robotframework_gemini-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Gemini-backed text and multimodal oracles for Robot Framework."""
|
|
2
|
+
|
|
3
|
+
from robotframework_gemini.legacy_env import (
|
|
4
|
+
ensure_gemini_env_from_legacy,
|
|
5
|
+
ensure_importable,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
ensure_importable()
|
|
9
|
+
ensure_gemini_env_from_legacy()
|
|
10
|
+
|
|
11
|
+
from robotframework_gemini.client import GeminiOrchestrator
|
|
12
|
+
from robotframework_gemini.library import BrowserGeminiLibrary, GeminiLibrary
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"GeminiOrchestrator",
|
|
16
|
+
"GeminiLibrary",
|
|
17
|
+
"BrowserGeminiLibrary",
|
|
18
|
+
"__version__",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Capture screenshot / page source from an active Robot Framework Browser library."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_open_browser_library():
|
|
13
|
+
"""Return the live Browser library instance (Playwright) from Robot BuiltIn."""
|
|
14
|
+
try:
|
|
15
|
+
from robot.libraries.BuiltIn import BuiltIn
|
|
16
|
+
except ImportError as e:
|
|
17
|
+
raise RuntimeError("robotframework-gemini: Robot Framework not available") from e
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
return BuiltIn().get_library_instance("Browser")
|
|
21
|
+
except Exception as e:
|
|
22
|
+
raise RuntimeError(
|
|
23
|
+
"robotframework-gemini: Browser library is not open or not imported as 'Browser'"
|
|
24
|
+
) from e
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def take_screenshot_to_path(
|
|
28
|
+
output_path: str | Path,
|
|
29
|
+
*,
|
|
30
|
+
selector: str | None = None,
|
|
31
|
+
) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Call Browser.take_screenshot and return the path returned by the library.
|
|
34
|
+
|
|
35
|
+
`selector` matches Browser kwarg when capturing an element region.
|
|
36
|
+
"""
|
|
37
|
+
browser = get_open_browser_library()
|
|
38
|
+
kw: dict[str, Any] = {"filename": str(output_path)}
|
|
39
|
+
if selector:
|
|
40
|
+
kw["selector"] = selector
|
|
41
|
+
path = browser.take_screenshot(**kw)
|
|
42
|
+
logger.debug("Screenshot written: %s", path)
|
|
43
|
+
return str(path)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_page_source_text() -> str:
|
|
47
|
+
browser = get_open_browser_library()
|
|
48
|
+
if not hasattr(browser, "get_page_source"):
|
|
49
|
+
raise RuntimeError("Browser library has no get_page_source")
|
|
50
|
+
return browser.get_page_source()
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Gemini client: multimodal (image file) and text-only calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import mimetypes
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from google import genai
|
|
11
|
+
from google.genai import types
|
|
12
|
+
|
|
13
|
+
from robotframework_gemini.prompt_build import (
|
|
14
|
+
DEFAULT_RATING_OUTPUT_INSTRUCTIONS,
|
|
15
|
+
build_evaluation_prompt,
|
|
16
|
+
merge_extra_instructions,
|
|
17
|
+
)
|
|
18
|
+
from robotframework_gemini.response_parse import normalize_rating
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
_DEFAULT_IMAGE_MIME = "image/png"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _mime_for_path(path: Path) -> str:
|
|
26
|
+
mime, _ = mimetypes.guess_type(path.name)
|
|
27
|
+
return mime or _DEFAULT_IMAGE_MIME
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GeminiOrchestrator:
|
|
31
|
+
"""
|
|
32
|
+
Thin wrapper around google-genai for test oracles.
|
|
33
|
+
|
|
34
|
+
Environment:
|
|
35
|
+
- GEMINI_API_KEY for the API key (unless ``api_key`` is passed explicitly)
|
|
36
|
+
- GEMINI_MODEL for model id (e.g. gemini-2.0-flash); defaults to gemini-2.0-flash
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
api_key: str | None = None,
|
|
43
|
+
model: str | None = None,
|
|
44
|
+
):
|
|
45
|
+
key = api_key or os.environ.get("GEMINI_API_KEY")
|
|
46
|
+
if not key:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"Missing API key: set GEMINI_API_KEY (or pass api_key=...)"
|
|
49
|
+
)
|
|
50
|
+
self.model = model or os.environ.get("GEMINI_MODEL") or "gemini-2.0-flash"
|
|
51
|
+
self._client = genai.Client(api_key=key)
|
|
52
|
+
|
|
53
|
+
def generate_from_text(self, prompt: str) -> str:
|
|
54
|
+
response = self._client.models.generate_content(model=self.model, contents=prompt)
|
|
55
|
+
text = (response.text or "").strip()
|
|
56
|
+
logger.debug("generate_from_text chars=%s", len(text))
|
|
57
|
+
return text
|
|
58
|
+
|
|
59
|
+
def generate_with_image_file(self, prompt: str, image_path: str | Path) -> str:
|
|
60
|
+
path = Path(image_path)
|
|
61
|
+
if not path.is_file():
|
|
62
|
+
raise FileNotFoundError(f"Image not found: {path}")
|
|
63
|
+
data = path.read_bytes()
|
|
64
|
+
mime = _mime_for_path(path)
|
|
65
|
+
parts = [
|
|
66
|
+
types.Part.from_text(text=prompt),
|
|
67
|
+
types.Part.from_bytes(data=data, mime_type=mime),
|
|
68
|
+
]
|
|
69
|
+
response = self._client.models.generate_content(
|
|
70
|
+
model=self.model,
|
|
71
|
+
contents=parts,
|
|
72
|
+
)
|
|
73
|
+
text = (response.text or "").strip()
|
|
74
|
+
logger.debug("generate_with_image_file chars=%s mime=%s", len(text), mime)
|
|
75
|
+
return text
|
|
76
|
+
|
|
77
|
+
def generate_with_image_and_html(
|
|
78
|
+
self,
|
|
79
|
+
prompt: str,
|
|
80
|
+
image_path: str | Path,
|
|
81
|
+
html_text: str,
|
|
82
|
+
*,
|
|
83
|
+
max_html_chars: int = 120_000,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Multimodal image + truncated HTML as additional text part."""
|
|
86
|
+
path = Path(image_path)
|
|
87
|
+
if not path.is_file():
|
|
88
|
+
raise FileNotFoundError(f"Image not found: {path}")
|
|
89
|
+
data = path.read_bytes()
|
|
90
|
+
mime = _mime_for_path(path)
|
|
91
|
+
html_snip = html_text[:max_html_chars] if html_text else ""
|
|
92
|
+
combined_prompt = f"{prompt}\n\n--- HTML (truncado) ---\n{html_snip}"
|
|
93
|
+
parts = [
|
|
94
|
+
types.Part.from_text(text=combined_prompt),
|
|
95
|
+
types.Part.from_bytes(data=data, mime_type=mime),
|
|
96
|
+
]
|
|
97
|
+
response = self._client.models.generate_content(
|
|
98
|
+
model=self.model,
|
|
99
|
+
contents=parts,
|
|
100
|
+
)
|
|
101
|
+
return (response.text or "").strip()
|
|
102
|
+
|
|
103
|
+
def evaluate_with_image(
|
|
104
|
+
self,
|
|
105
|
+
context: str,
|
|
106
|
+
evaluation: str,
|
|
107
|
+
image_path: str | Path,
|
|
108
|
+
*,
|
|
109
|
+
extra_instructions: str | None = None,
|
|
110
|
+
) -> str:
|
|
111
|
+
prompt = build_evaluation_prompt(
|
|
112
|
+
context,
|
|
113
|
+
evaluation,
|
|
114
|
+
extra_instructions=extra_instructions,
|
|
115
|
+
include_visual_focus=True,
|
|
116
|
+
)
|
|
117
|
+
return self.generate_with_image_file(prompt, image_path)
|
|
118
|
+
|
|
119
|
+
def evaluate_with_image_and_html(
|
|
120
|
+
self,
|
|
121
|
+
context: str,
|
|
122
|
+
evaluation: str,
|
|
123
|
+
image_path: str | Path,
|
|
124
|
+
html_text: str,
|
|
125
|
+
*,
|
|
126
|
+
extra_instructions: str | None = None,
|
|
127
|
+
max_html_chars: int = 120_000,
|
|
128
|
+
) -> str:
|
|
129
|
+
prompt = build_evaluation_prompt(
|
|
130
|
+
context,
|
|
131
|
+
evaluation,
|
|
132
|
+
extra_instructions=extra_instructions,
|
|
133
|
+
include_visual_focus=True,
|
|
134
|
+
)
|
|
135
|
+
return self.generate_with_image_and_html(
|
|
136
|
+
prompt, image_path, html_text, max_html_chars=max_html_chars
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def evaluate_with_text(
|
|
140
|
+
self,
|
|
141
|
+
context: str,
|
|
142
|
+
evaluation: str,
|
|
143
|
+
*,
|
|
144
|
+
extra_instructions: str | None = None,
|
|
145
|
+
) -> str:
|
|
146
|
+
prompt = build_evaluation_prompt(
|
|
147
|
+
context,
|
|
148
|
+
evaluation,
|
|
149
|
+
extra_instructions=extra_instructions,
|
|
150
|
+
include_visual_focus=False,
|
|
151
|
+
)
|
|
152
|
+
return self.generate_from_text(prompt)
|
|
153
|
+
|
|
154
|
+
def evaluate_with_text_rating(
|
|
155
|
+
self,
|
|
156
|
+
context: str,
|
|
157
|
+
evaluation: str,
|
|
158
|
+
*,
|
|
159
|
+
extra_instructions: str | None = None,
|
|
160
|
+
) -> str:
|
|
161
|
+
"""Generic text-only judge: score how well ``context`` meets ``evaluation`` on a 1–5 scale."""
|
|
162
|
+
rating_extra = merge_extra_instructions(
|
|
163
|
+
DEFAULT_RATING_OUTPUT_INSTRUCTIONS,
|
|
164
|
+
extra_instructions,
|
|
165
|
+
)
|
|
166
|
+
return self.evaluate_with_text(
|
|
167
|
+
context,
|
|
168
|
+
evaluation,
|
|
169
|
+
extra_instructions=rating_extra,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def parse_rating(raw: str) -> str:
|
|
174
|
+
return normalize_rating(raw)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Map legacy QA env vars to GEMINI_* and ensure the package is importable in Docker mounts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ensure_importable() -> None:
|
|
11
|
+
"""Add ``src/`` to ``sys.path`` when the package is not installed (repo volume mount)."""
|
|
12
|
+
try:
|
|
13
|
+
import robotframework_gemini.client # noqa: F401
|
|
14
|
+
return
|
|
15
|
+
except ImportError:
|
|
16
|
+
pass
|
|
17
|
+
src = Path(__file__).resolve().parent.parent
|
|
18
|
+
src_str = str(src)
|
|
19
|
+
if src_str not in sys.path:
|
|
20
|
+
sys.path.insert(0, src_str)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ensure_gemini_env_from_legacy() -> None:
|
|
24
|
+
"""
|
|
25
|
+
Set GEMINI_API_KEY / GEMINI_MODEL from names used by NCM PRO and LIA suites
|
|
26
|
+
when the new variables are not already defined.
|
|
27
|
+
"""
|
|
28
|
+
if not os.environ.get("GEMINI_API_KEY"):
|
|
29
|
+
for key in ("LLM_API_KEY", "LLM_API_KEY_LIA"):
|
|
30
|
+
value = os.environ.get(key)
|
|
31
|
+
if value:
|
|
32
|
+
os.environ["GEMINI_API_KEY"] = value
|
|
33
|
+
break
|
|
34
|
+
if not os.environ.get("GEMINI_MODEL"):
|
|
35
|
+
value = os.environ.get("LLM_LIA_MODEL")
|
|
36
|
+
if value:
|
|
37
|
+
os.environ["GEMINI_MODEL"] = value
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Robot Framework library: Gemini oracles (text-only and optional Browser screenshots)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from robot.api.deco import keyword
|
|
12
|
+
except ImportError: # pragma: no cover
|
|
13
|
+
|
|
14
|
+
def keyword(name: str | None = None, tags=()) -> Any:
|
|
15
|
+
def decorator(func):
|
|
16
|
+
return func
|
|
17
|
+
|
|
18
|
+
return decorator
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from robotframework_gemini.legacy_env import (
|
|
22
|
+
ensure_gemini_env_from_legacy,
|
|
23
|
+
ensure_importable,
|
|
24
|
+
)
|
|
25
|
+
from robotframework_gemini.browser_helpers import (
|
|
26
|
+
get_page_source_text,
|
|
27
|
+
take_screenshot_to_path,
|
|
28
|
+
)
|
|
29
|
+
from robotframework_gemini.client import GeminiOrchestrator
|
|
30
|
+
|
|
31
|
+
ensure_importable()
|
|
32
|
+
ensure_gemini_env_from_legacy()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GeminiLibrary:
|
|
36
|
+
"""Keywords for Gemini text oracles and optional Browser screenshot capture."""
|
|
37
|
+
|
|
38
|
+
ROBOT_LIBRARY_SCOPE = "GLOBAL"
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
api_key: str | None = None,
|
|
43
|
+
model: str | None = None,
|
|
44
|
+
):
|
|
45
|
+
self._api_key = api_key if api_key and str(api_key).strip() else None
|
|
46
|
+
self._model = model if model and str(model).strip() else None
|
|
47
|
+
self._orchestrator: GeminiOrchestrator | None = None
|
|
48
|
+
|
|
49
|
+
def _get_orchestrator(self) -> GeminiOrchestrator:
|
|
50
|
+
if self._orchestrator is None:
|
|
51
|
+
self._orchestrator = GeminiOrchestrator(
|
|
52
|
+
api_key=self._api_key,
|
|
53
|
+
model=self._model,
|
|
54
|
+
)
|
|
55
|
+
return self._orchestrator
|
|
56
|
+
|
|
57
|
+
# --- Generic evaluation (context + criteria) ---
|
|
58
|
+
|
|
59
|
+
@keyword("Gemini Evaluate With Image File")
|
|
60
|
+
def gemini_evaluate_with_image_file(
|
|
61
|
+
self,
|
|
62
|
+
context: str,
|
|
63
|
+
evaluation: str,
|
|
64
|
+
image_path: str,
|
|
65
|
+
extra_instructions: str | None = None,
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Send context + evaluation with an existing image file (multimodal)."""
|
|
68
|
+
return self._get_orchestrator().evaluate_with_image(
|
|
69
|
+
context,
|
|
70
|
+
evaluation,
|
|
71
|
+
image_path,
|
|
72
|
+
extra_instructions=extra_instructions,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@keyword("Gemini Evaluate With Screen")
|
|
76
|
+
def gemini_evaluate_with_screen(
|
|
77
|
+
self,
|
|
78
|
+
context: str,
|
|
79
|
+
evaluation: str,
|
|
80
|
+
selector: str | None = None,
|
|
81
|
+
filename: str | None = None,
|
|
82
|
+
extra_instructions: str | None = None,
|
|
83
|
+
) -> str:
|
|
84
|
+
"""Take a Browser screenshot, then evaluate with context + criteria."""
|
|
85
|
+
path = self._resolve_screenshot_path(filename)
|
|
86
|
+
take_screenshot_to_path(path, selector=selector)
|
|
87
|
+
return self._get_orchestrator().evaluate_with_image(
|
|
88
|
+
context,
|
|
89
|
+
evaluation,
|
|
90
|
+
path,
|
|
91
|
+
extra_instructions=extra_instructions,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@keyword("Gemini Evaluate With Screen And Html")
|
|
95
|
+
def gemini_evaluate_with_screen_and_html(
|
|
96
|
+
self,
|
|
97
|
+
context: str,
|
|
98
|
+
evaluation: str,
|
|
99
|
+
include_html: bool = True,
|
|
100
|
+
filename: str | None = None,
|
|
101
|
+
extra_instructions: str | None = None,
|
|
102
|
+
) -> str:
|
|
103
|
+
"""Screenshot plus optional DOM HTML (truncated in client) for richer context."""
|
|
104
|
+
path = self._resolve_screenshot_path(filename)
|
|
105
|
+
take_screenshot_to_path(path)
|
|
106
|
+
orch = self._get_orchestrator()
|
|
107
|
+
if include_html:
|
|
108
|
+
html = get_page_source_text()
|
|
109
|
+
return orch.evaluate_with_image_and_html(
|
|
110
|
+
context,
|
|
111
|
+
evaluation,
|
|
112
|
+
path,
|
|
113
|
+
html,
|
|
114
|
+
extra_instructions=extra_instructions,
|
|
115
|
+
)
|
|
116
|
+
return orch.evaluate_with_image(
|
|
117
|
+
context,
|
|
118
|
+
evaluation,
|
|
119
|
+
path,
|
|
120
|
+
extra_instructions=extra_instructions,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@keyword("Gemini Evaluate Text")
|
|
124
|
+
def gemini_evaluate_text(
|
|
125
|
+
self,
|
|
126
|
+
context: str,
|
|
127
|
+
evaluation: str,
|
|
128
|
+
extra_instructions: str | None = None,
|
|
129
|
+
) -> str:
|
|
130
|
+
"""Text-only oracle from context + evaluation (no screenshot)."""
|
|
131
|
+
return self._get_orchestrator().evaluate_with_text(
|
|
132
|
+
context,
|
|
133
|
+
evaluation,
|
|
134
|
+
extra_instructions=extra_instructions,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@keyword("Gemini Evaluate Text Verdict")
|
|
138
|
+
def gemini_evaluate_text_verdict(
|
|
139
|
+
self,
|
|
140
|
+
context: str,
|
|
141
|
+
evaluation: str,
|
|
142
|
+
output_instructions: str,
|
|
143
|
+
) -> str:
|
|
144
|
+
"""Text judge: ``output_instructions`` defines the verdict format (e.g. Sim/Não/Aviso).
|
|
145
|
+
|
|
146
|
+
Returns raw model text. The test asserts the verdict (e.g. first line with ``Get Line``).
|
|
147
|
+
"""
|
|
148
|
+
return self._get_orchestrator().evaluate_with_text(
|
|
149
|
+
context,
|
|
150
|
+
evaluation,
|
|
151
|
+
extra_instructions=output_instructions,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@keyword("Gemini Evaluate Text Rating")
|
|
155
|
+
def gemini_evaluate_text_rating(
|
|
156
|
+
self,
|
|
157
|
+
context: str,
|
|
158
|
+
evaluation: str,
|
|
159
|
+
extra_instructions: str | None = None,
|
|
160
|
+
) -> str:
|
|
161
|
+
"""Generic text judge with a 1–5 rubric (returns raw SCORE/REASON lines).
|
|
162
|
+
|
|
163
|
+
``context`` — any test evidence (API body, logs, UI text, file excerpt, etc.).
|
|
164
|
+
``evaluation`` — criterion to score (how well the evidence meets the test intent).
|
|
165
|
+
Use ``Gemini Parse Rating`` to extract the integer score (1–5).
|
|
166
|
+
``extra_instructions`` is appended after the built-in rubric.
|
|
167
|
+
"""
|
|
168
|
+
return self._get_orchestrator().evaluate_with_text_rating(
|
|
169
|
+
context,
|
|
170
|
+
evaluation,
|
|
171
|
+
extra_instructions=extra_instructions,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@keyword("Gemini Parse Rating")
|
|
175
|
+
def gemini_parse_rating(self, raw_response: str) -> str:
|
|
176
|
+
"""Extract score 1–5 from a judge response; returns raw text if parsing fails."""
|
|
177
|
+
return GeminiOrchestrator.parse_rating(raw_response)
|
|
178
|
+
|
|
179
|
+
@keyword("Gemini Generate From Prompt")
|
|
180
|
+
def gemini_generate_from_prompt(self, prompt: str) -> str:
|
|
181
|
+
"""Send a single text prompt and return the model reply (no context/evaluation template)."""
|
|
182
|
+
return self._get_orchestrator().generate_from_text(prompt)
|
|
183
|
+
|
|
184
|
+
# --- Single-prompt shortcuts (advanced) ---
|
|
185
|
+
|
|
186
|
+
@keyword("Gemini Screenshot And Ask")
|
|
187
|
+
def gemini_screenshot_and_ask(
|
|
188
|
+
self,
|
|
189
|
+
prompt: str,
|
|
190
|
+
selector: str | None = None,
|
|
191
|
+
filename: str | None = None,
|
|
192
|
+
) -> str:
|
|
193
|
+
"""
|
|
194
|
+
Take a screenshot with Browser library, send PNG + prompt to Gemini, return text.
|
|
195
|
+
|
|
196
|
+
If ``filename`` is omitted, a temp PNG under the system temp dir is used.
|
|
197
|
+
Prefer ``Gemini Evaluate With Screen`` with separate context and evaluation.
|
|
198
|
+
"""
|
|
199
|
+
path = self._resolve_screenshot_path(filename)
|
|
200
|
+
take_screenshot_to_path(path, selector=selector)
|
|
201
|
+
return self._get_orchestrator().generate_with_image_file(prompt, path)
|
|
202
|
+
|
|
203
|
+
@keyword("Gemini Screenshot Html And Ask")
|
|
204
|
+
def gemini_screenshot_html_and_ask(self, prompt: str, filename: str | None = None) -> str:
|
|
205
|
+
"""Screenshot full page + current page HTML (truncated inside client)."""
|
|
206
|
+
path = self._resolve_screenshot_path(filename)
|
|
207
|
+
take_screenshot_to_path(path)
|
|
208
|
+
html = get_page_source_text()
|
|
209
|
+
return self._get_orchestrator().generate_with_image_and_html(prompt, path, html)
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def _resolve_screenshot_path(filename: str | None) -> Path:
|
|
213
|
+
if filename:
|
|
214
|
+
return Path(filename)
|
|
215
|
+
fd, name = tempfile.mkstemp(prefix="robotframework-gemini-", suffix=".png")
|
|
216
|
+
os.close(fd)
|
|
217
|
+
return Path(name)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Backward-compatible alias for suites that import BrowserGeminiLibrary.
|
|
221
|
+
BrowserGeminiLibrary = GeminiLibrary
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Compose neutral Portuguese prompts from context + evaluation criteria."""
|
|
2
|
+
|
|
3
|
+
DEFAULT_RATING_OUTPUT_INSTRUCTIONS = """\
|
|
4
|
+
Atribua uma nota inteira de 1 a 5 julgando o contexto fornecido apenas pelos critérios acima.
|
|
5
|
+
O contexto pode ser qualquer evidência de teste (resposta de API, trecho de log, texto de UI, \
|
|
6
|
+
transcrição, JSON, markdown, etc.) — não presuma formato de pergunta e resposta.
|
|
7
|
+
|
|
8
|
+
Formato obrigatório da resposta (exatamente duas linhas):
|
|
9
|
+
SCORE: <1|2|3|4|5>
|
|
10
|
+
REASON: <uma frase curta justificando a nota>
|
|
11
|
+
|
|
12
|
+
Rubrica (quão bem o contexto atende ao critério em ## O que avaliar):
|
|
13
|
+
1 — Não atende: falha grave ou critério central ausente.
|
|
14
|
+
2 — Atende minimamente: lacunas importantes ou incoerências relevantes.
|
|
15
|
+
3 — Atende parcialmente: cumpre parte do critério, com ressalvas claras.
|
|
16
|
+
4 — Atende bem: cumpre o critério com pequenas imperfeições.
|
|
17
|
+
5 — Atende plenamente: cumpre o critério de forma clara e completa.
|
|
18
|
+
|
|
19
|
+
Regras:
|
|
20
|
+
- Use somente inteiros de 1 a 5 em SCORE (sem decimais, sem escala 0–10).
|
|
21
|
+
- Não inclua texto antes da linha SCORE.
|
|
22
|
+
- Baseie-se apenas nas evidências presentes no contexto; não invente fatos."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def merge_extra_instructions(
|
|
26
|
+
base: str,
|
|
27
|
+
extra_instructions: str | None = None,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""Append caller overrides after structured base instructions."""
|
|
30
|
+
parts = [base.strip()]
|
|
31
|
+
if extra_instructions and extra_instructions.strip():
|
|
32
|
+
parts.append(extra_instructions.strip())
|
|
33
|
+
return "\n\n".join(parts)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_evaluation_prompt(
|
|
37
|
+
context: str,
|
|
38
|
+
evaluation: str,
|
|
39
|
+
*,
|
|
40
|
+
extra_instructions: str | None = None,
|
|
41
|
+
include_visual_focus: bool = False,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Build a single prompt from caller-supplied context and what to judge.
|
|
45
|
+
|
|
46
|
+
When ``include_visual_focus`` is True (screenshot/multimodal), remind the model
|
|
47
|
+
to rely only on what is visibly rendered.
|
|
48
|
+
"""
|
|
49
|
+
parts: list[str] = []
|
|
50
|
+
|
|
51
|
+
if include_visual_focus:
|
|
52
|
+
parts.append(
|
|
53
|
+
"Uma captura de tela acompanha esta mensagem. Baseie-se apenas no que está "
|
|
54
|
+
"claramente visível na imagem (textos, números, cores e layout renderizado). "
|
|
55
|
+
"Não invente detalhes que não apareçam na captura."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
parts.extend(
|
|
59
|
+
[
|
|
60
|
+
"## Contexto",
|
|
61
|
+
context.strip(),
|
|
62
|
+
"",
|
|
63
|
+
"## O que avaliar",
|
|
64
|
+
evaluation.strip(),
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if extra_instructions and extra_instructions.strip():
|
|
69
|
+
parts.extend(["", "## Instruções adicionais de resposta", extra_instructions.strip()])
|
|
70
|
+
|
|
71
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Normalize structured LLM outputs (e.g. 1–5 ratings) for optional test assertions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
_SCORE_LINE = re.compile(
|
|
8
|
+
r"(?im)^\s*(?:score|nota)\s*:\s*([1-5])\s*$"
|
|
9
|
+
)
|
|
10
|
+
_SCORE_FRACTION = re.compile(r"(?i)\b([1-5])\s*/\s*5\b")
|
|
11
|
+
_STANDALONE_SCORE = re.compile(r"(?m)^\s*([1-5])\s*$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize_rating(raw: str) -> str:
|
|
15
|
+
"""Extract an integer score from 1 to 5 when the model follows the rating format."""
|
|
16
|
+
text = (raw or "").strip()
|
|
17
|
+
if not text:
|
|
18
|
+
return text
|
|
19
|
+
|
|
20
|
+
match = _SCORE_LINE.search(text)
|
|
21
|
+
if match:
|
|
22
|
+
return match.group(1)
|
|
23
|
+
|
|
24
|
+
for pattern in (_SCORE_FRACTION, _STANDALONE_SCORE):
|
|
25
|
+
match = pattern.search(text)
|
|
26
|
+
if match:
|
|
27
|
+
return match.group(1)
|
|
28
|
+
|
|
29
|
+
inline = re.search(r"(?i)(?:score|nota)\s*:\s*([1-5])\b", text)
|
|
30
|
+
if inline:
|
|
31
|
+
return inline.group(1)
|
|
32
|
+
|
|
33
|
+
return text
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotframework-gemini
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Google Gemini oracles for Robot Framework: text-only and multimodal (image) assertions.
|
|
5
|
+
Project-URL: Homepage, https://github.com/carlosnizolli/robotframework-gemini
|
|
6
|
+
Project-URL: Documentation, https://github.com/carlosnizolli/robotframework-gemini#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/carlosnizolli/robotframework-gemini
|
|
8
|
+
Project-URL: Issues, https://github.com/carlosnizolli/robotframework-gemini/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/carlosnizolli/robotframework-gemini/releases
|
|
10
|
+
Author: Carlos Nizolli
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: Gemini,LLM,QA,Robot Framework,multimodal,testing
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: Robot Framework
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Testing
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: google-genai>=1.10.0
|
|
27
|
+
Provides-Extra: browser
|
|
28
|
+
Requires-Dist: robotframework-browser>=18.0.0; extra == 'browser'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# robotframework-gemini
|
|
36
|
+
|
|
37
|
+
Biblioteca **Python** e **keywords do Robot Framework** para oráculos com **Google Gemini**: avaliação **só texto** (API, logs, JSON, etc.) ou **multimodal** com imagem (arquivo PNG ou captura do **Robot Framework Browser**).
|
|
38
|
+
|
|
39
|
+
- **Multimodal**: envia o texto do prompt e a captura como `Part` em bytes (`google-genai`).
|
|
40
|
+
- **Fluxo recomendado**: duas entradas — **contexto** (enquadramento do teste) e **avaliação** (critério ou pergunta objetiva).
|
|
41
|
+
- **Browser opcional**: keywords de screenshot exigem `Library Browser`; keywords de texto funcionam sem navegador.
|
|
42
|
+
|
|
43
|
+
## Instalação
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install robotframework-gemini
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Com suporte a captura via Robot Framework Browser (Playwright):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install "robotframework-gemini[browser]"
|
|
53
|
+
python -m pip install robotframework
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Desenvolvimento local:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python -m pip install -e ".[dev]"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Variáveis de ambiente
|
|
63
|
+
|
|
64
|
+
| Variável | Função |
|
|
65
|
+
|-------------------|---------------------------------------------|
|
|
66
|
+
| `GEMINI_API_KEY` | Chave da API Gemini (obrigatória se não passar `api_key` na Library) |
|
|
67
|
+
| `GEMINI_MODEL` | Modelo (ex.: `gemini-2.0-flash`). Se omitido, usa `gemini-2.0-flash`. |
|
|
68
|
+
|
|
69
|
+
## Uso em Python (`GeminiOrchestrator`)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from pathlib import Path
|
|
73
|
+
from robotframework_gemini import GeminiOrchestrator
|
|
74
|
+
|
|
75
|
+
orc = GeminiOrchestrator()
|
|
76
|
+
ctx = "Web dashboard with the 'Active' category filter applied."
|
|
77
|
+
crit = "Do the visible list items match the selected category?"
|
|
78
|
+
raw = orc.evaluate_with_image(ctx, crit, Path("screen.png"))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Para formato de saída restrito (ex.: Yes/No), use `extra_instructions`:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
raw = orc.evaluate_with_text(
|
|
85
|
+
ctx,
|
|
86
|
+
crit,
|
|
87
|
+
extra_instructions="Reply with one word only: Yes or No.",
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Uso no Robot Framework
|
|
92
|
+
|
|
93
|
+
### Só texto (sem Browser)
|
|
94
|
+
|
|
95
|
+
```robot
|
|
96
|
+
*** Settings ***
|
|
97
|
+
Library GeminiLibrary
|
|
98
|
+
|
|
99
|
+
*** Keywords ***
|
|
100
|
+
Validar resposta da API
|
|
101
|
+
${ctx}= Set Variable {"status": "ok", "items": 3}
|
|
102
|
+
${crit}= Set Variable O payload indica sucesso com itens?
|
|
103
|
+
${raw}= Gemini Evaluate Text ${ctx} ${crit}
|
|
104
|
+
Log ${raw}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Com captura de tela (Browser)
|
|
108
|
+
|
|
109
|
+
Declare a biblioteca **Browser** antes de **Gemini**:
|
|
110
|
+
|
|
111
|
+
```robot
|
|
112
|
+
*** Settings ***
|
|
113
|
+
Library Browser
|
|
114
|
+
Library GeminiLibrary
|
|
115
|
+
|
|
116
|
+
*** Keywords ***
|
|
117
|
+
Checar tela por critério neutro
|
|
118
|
+
${ctx}= Set Variable Lista filtrada por status Ativo.
|
|
119
|
+
${crit}= Set Variable Todos os itens visíveis mostram status Ativo?
|
|
120
|
+
${raw}= Gemini Evaluate With Screen ${ctx} ${crit}
|
|
121
|
+
Log ${raw}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Import explícito (equivalente):
|
|
125
|
+
|
|
126
|
+
```robot
|
|
127
|
+
Library robotframework_gemini.library.GeminiLibrary
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Com arquivo já salvo:
|
|
131
|
+
|
|
132
|
+
```robot
|
|
133
|
+
Browser.Take Screenshot ${OUTPUT_DIR}/tela.png
|
|
134
|
+
${raw}= Gemini Evaluate With Image File ${ctx} ${crit} ${OUTPUT_DIR}/tela.png
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Veredito via prompt (primeira linha) e nota 1–5:
|
|
138
|
+
|
|
139
|
+
```robot
|
|
140
|
+
${raw}= Gemini Evaluate With Screen ${ctx} ${crit}
|
|
141
|
+
... extra_instructions=Responda com uma palavra na primeira linha: Sim ou Não.
|
|
142
|
+
${v}= Get Line ${raw} 0
|
|
143
|
+
Should Be Equal As Strings ${v} Sim
|
|
144
|
+
|
|
145
|
+
${raw}= Gemini Evaluate Text Rating ${ctx} ${criterion}
|
|
146
|
+
${score}= Gemini Parse Rating ${raw}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Consulte também [`examples/demo_template.robot`](examples/demo_template.robot).
|
|
150
|
+
|
|
151
|
+
## Documentação de Keywords
|
|
152
|
+
|
|
153
|
+
- Português: [`docs/KEYWORDS.pt-BR.md`](docs/KEYWORDS.pt-BR.md)
|
|
154
|
+
- English: [`docs/KEYWORDS.en.md`](docs/KEYWORDS.en.md)
|
|
155
|
+
|
|
156
|
+
## Compatibilidade
|
|
157
|
+
|
|
158
|
+
- `BrowserGeminiLibrary` permanece como alias de `GeminiLibrary` para suítes existentes.
|
|
159
|
+
- Variáveis legadas (`LLM_API_KEY`, `LLM_API_KEY_LIA`, `LLM_LIA_MODEL`) são mapeadas para `GEMINI_*` ao importar o pacote.
|
|
160
|
+
|
|
161
|
+
## Testes
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pytest
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Os testes usam mocks de `generate_content`; não há chamadas reais à API.
|
|
168
|
+
|
|
169
|
+
## Licença
|
|
170
|
+
|
|
171
|
+
MIT.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
robotframework_gemini/__init__.py,sha256=KsKuSZ19B5dTMFWVudY3zdLHXVP3wsh_gDKZdwBmJmY,501
|
|
2
|
+
robotframework_gemini/browser_helpers.py,sha256=mfwvxIoA2EkcVRkUKDzcAab8Uzw2Si6BKUz9INJR800,1523
|
|
3
|
+
robotframework_gemini/client.py,sha256=auLJwqzRa_pOdTwt4GYzAOXso_aXQmURXkEUVx6ZhiA,5345
|
|
4
|
+
robotframework_gemini/legacy_env.py,sha256=Gy786rqi4up4KooxNEHDlyI9rBSitTOahnL5ew0dsBw,1149
|
|
5
|
+
robotframework_gemini/library.py,sha256=VyIP6Rgoc9WVamcK6-h9ABhoG1qk8tfnMydbS4Lck94,7553
|
|
6
|
+
robotframework_gemini/prompt_build.py,sha256=iONTz5b41clNMMV9KX0KIXj7lVxD82FK3BHmLHrJnzI,2582
|
|
7
|
+
robotframework_gemini/response_parse.py,sha256=tKovgjnPinUaKtA5LPdlt9cofZPqmw55Zr8quO07MyU,886
|
|
8
|
+
robotframework_gemini-0.2.0.dist-info/METADATA,sha256=4gCnXjuHgg7D8mzb3xaXJN86oJoggZnioxlafSOazaw,5471
|
|
9
|
+
robotframework_gemini-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
10
|
+
robotframework_gemini-0.2.0.dist-info/entry_points.txt,sha256=-1pcYw5FISk175IbQ36Amn58c3smC-r_RsSvTkzk4BI,87
|
|
11
|
+
robotframework_gemini-0.2.0.dist-info/licenses/LICENSE,sha256=Eb9RR6NPLyY_f2yiTrI-rB3qGBnItU5DJZaReAGrJ_U,1071
|
|
12
|
+
robotframework_gemini-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carlos Nizolli
|
|
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.
|