robotframework-gemini 0.2.0__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ /dist/
5
+ /*.egg-info/
6
+ /.pytest_cache/
@@ -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.
@@ -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,137 @@
1
+ # robotframework-gemini
2
+
3
+ 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**).
4
+
5
+ - **Multimodal**: envia o texto do prompt e a captura como `Part` em bytes (`google-genai`).
6
+ - **Fluxo recomendado**: duas entradas — **contexto** (enquadramento do teste) e **avaliação** (critério ou pergunta objetiva).
7
+ - **Browser opcional**: keywords de screenshot exigem `Library Browser`; keywords de texto funcionam sem navegador.
8
+
9
+ ## Instalação
10
+
11
+ ```bash
12
+ pip install robotframework-gemini
13
+ ```
14
+
15
+ Com suporte a captura via Robot Framework Browser (Playwright):
16
+
17
+ ```bash
18
+ pip install "robotframework-gemini[browser]"
19
+ python -m pip install robotframework
20
+ ```
21
+
22
+ Desenvolvimento local:
23
+
24
+ ```bash
25
+ python -m pip install -e ".[dev]"
26
+ ```
27
+
28
+ ## Variáveis de ambiente
29
+
30
+ | Variável | Função |
31
+ |-------------------|---------------------------------------------|
32
+ | `GEMINI_API_KEY` | Chave da API Gemini (obrigatória se não passar `api_key` na Library) |
33
+ | `GEMINI_MODEL` | Modelo (ex.: `gemini-2.0-flash`). Se omitido, usa `gemini-2.0-flash`. |
34
+
35
+ ## Uso em Python (`GeminiOrchestrator`)
36
+
37
+ ```python
38
+ from pathlib import Path
39
+ from robotframework_gemini import GeminiOrchestrator
40
+
41
+ orc = GeminiOrchestrator()
42
+ ctx = "Web dashboard with the 'Active' category filter applied."
43
+ crit = "Do the visible list items match the selected category?"
44
+ raw = orc.evaluate_with_image(ctx, crit, Path("screen.png"))
45
+ ```
46
+
47
+ Para formato de saída restrito (ex.: Yes/No), use `extra_instructions`:
48
+
49
+ ```python
50
+ raw = orc.evaluate_with_text(
51
+ ctx,
52
+ crit,
53
+ extra_instructions="Reply with one word only: Yes or No.",
54
+ )
55
+ ```
56
+
57
+ ## Uso no Robot Framework
58
+
59
+ ### Só texto (sem Browser)
60
+
61
+ ```robot
62
+ *** Settings ***
63
+ Library GeminiLibrary
64
+
65
+ *** Keywords ***
66
+ Validar resposta da API
67
+ ${ctx}= Set Variable {"status": "ok", "items": 3}
68
+ ${crit}= Set Variable O payload indica sucesso com itens?
69
+ ${raw}= Gemini Evaluate Text ${ctx} ${crit}
70
+ Log ${raw}
71
+ ```
72
+
73
+ ### Com captura de tela (Browser)
74
+
75
+ Declare a biblioteca **Browser** antes de **Gemini**:
76
+
77
+ ```robot
78
+ *** Settings ***
79
+ Library Browser
80
+ Library GeminiLibrary
81
+
82
+ *** Keywords ***
83
+ Checar tela por critério neutro
84
+ ${ctx}= Set Variable Lista filtrada por status Ativo.
85
+ ${crit}= Set Variable Todos os itens visíveis mostram status Ativo?
86
+ ${raw}= Gemini Evaluate With Screen ${ctx} ${crit}
87
+ Log ${raw}
88
+ ```
89
+
90
+ Import explícito (equivalente):
91
+
92
+ ```robot
93
+ Library robotframework_gemini.library.GeminiLibrary
94
+ ```
95
+
96
+ Com arquivo já salvo:
97
+
98
+ ```robot
99
+ Browser.Take Screenshot ${OUTPUT_DIR}/tela.png
100
+ ${raw}= Gemini Evaluate With Image File ${ctx} ${crit} ${OUTPUT_DIR}/tela.png
101
+ ```
102
+
103
+ Veredito via prompt (primeira linha) e nota 1–5:
104
+
105
+ ```robot
106
+ ${raw}= Gemini Evaluate With Screen ${ctx} ${crit}
107
+ ... extra_instructions=Responda com uma palavra na primeira linha: Sim ou Não.
108
+ ${v}= Get Line ${raw} 0
109
+ Should Be Equal As Strings ${v} Sim
110
+
111
+ ${raw}= Gemini Evaluate Text Rating ${ctx} ${criterion}
112
+ ${score}= Gemini Parse Rating ${raw}
113
+ ```
114
+
115
+ Consulte também [`examples/demo_template.robot`](examples/demo_template.robot).
116
+
117
+ ## Documentação de Keywords
118
+
119
+ - Português: [`docs/KEYWORDS.pt-BR.md`](docs/KEYWORDS.pt-BR.md)
120
+ - English: [`docs/KEYWORDS.en.md`](docs/KEYWORDS.en.md)
121
+
122
+ ## Compatibilidade
123
+
124
+ - `BrowserGeminiLibrary` permanece como alias de `GeminiLibrary` para suítes existentes.
125
+ - Variáveis legadas (`LLM_API_KEY`, `LLM_API_KEY_LIA`, `LLM_LIA_MODEL`) são mapeadas para `GEMINI_*` ao importar o pacote.
126
+
127
+ ## Testes
128
+
129
+ ```bash
130
+ pytest
131
+ ```
132
+
133
+ Os testes usam mocks de `generate_content`; não há chamadas reais à API.
134
+
135
+ ## Licença
136
+
137
+ MIT.
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "robotframework-gemini"
7
+ version = "0.2.0"
8
+ description = "Google Gemini oracles for Robot Framework: text-only and multimodal (image) assertions."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Carlos Nizolli" }]
14
+ keywords = ["Robot Framework", "Gemini", "LLM", "multimodal", "QA", "testing"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Software Development :: Testing",
26
+ "Framework :: Robot Framework",
27
+ ]
28
+ dependencies = [
29
+ "google-genai>=1.10.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/carlosnizolli/robotframework-gemini"
34
+ Documentation = "https://github.com/carlosnizolli/robotframework-gemini#readme"
35
+ Repository = "https://github.com/carlosnizolli/robotframework-gemini"
36
+ Issues = "https://github.com/carlosnizolli/robotframework-gemini/issues"
37
+ Changelog = "https://github.com/carlosnizolli/robotframework-gemini/releases"
38
+
39
+ [project.optional-dependencies]
40
+ browser = [
41
+ "robotframework-browser>=18.0.0",
42
+ ]
43
+ dev = [
44
+ "pytest>=8.0",
45
+ "build>=1.0",
46
+ "twine>=5.0",
47
+ ]
48
+
49
+ [project.entry-points."robotframework.libraries"]
50
+ GeminiLibrary = "robotframework_gemini.library:GeminiLibrary"
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["src/robotframework_gemini"]
54
+
55
+ [tool.hatch.build.targets.sdist]
56
+ include = [
57
+ "src/robotframework_gemini",
58
+ "LICENSE",
59
+ "README.md",
60
+ ]
61
+
62
+ [tool.pytest.ini_options]
63
+ pythonpath = ["src"]
64
+ testpaths = ["tests"]
@@ -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