cellenium 0.1.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.
- cellenium/__init__.py +0 -0
- cellenium/ai/__init__.py +0 -0
- cellenium/ai/agents/__init__.py +0 -0
- cellenium/ai/agents/vision/__init__.py +0 -0
- cellenium/ai/agents/vision/config/agents.yaml +10 -0
- cellenium/ai/agents/vision/config/tasks.yaml +15 -0
- cellenium/ai/agents/vision/crew.py +33 -0
- cellenium/ai/agents/vision/schemas.py +41 -0
- cellenium/ai/agents/vision/skills/evaluator/SKILL.md +14 -0
- cellenium/ai/agents/vision/skills/vision/SKILL.md +19 -0
- cellenium/ai/agents/vision/tools/vision.py +167 -0
- cellenium/ai/config.py +17 -0
- cellenium/ai/tools/__init__.py +0 -0
- cellenium/ai/tools/image_encoder.py +10 -0
- cellenium/engine/__init__.py +0 -0
- cellenium/engine/manager.py +48 -0
- cellenium/engine/page_engine.py +79 -0
- cellenium/functions/__init__.py +0 -0
- cellenium/functions/locators.py +38 -0
- cellenium/functions/logger.py +44 -0
- cellenium/functions/sheets.py +71 -0
- cellenium/settings.py +26 -0
- cellenium-0.1.0.dist-info/METADATA +224 -0
- cellenium-0.1.0.dist-info/RECORD +26 -0
- cellenium-0.1.0.dist-info/WHEEL +4 -0
- cellenium-0.1.0.dist-info/licenses/LICENSE +7 -0
cellenium/__init__.py
ADDED
|
File without changes
|
cellenium/ai/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
vision:
|
|
2
|
+
role: >
|
|
3
|
+
Visual Evidence Analyst
|
|
4
|
+
goal: >
|
|
5
|
+
Extract precise, structured observations from supplied images without
|
|
6
|
+
inventing details that are not visibly supported.
|
|
7
|
+
backstory: >
|
|
8
|
+
You are a meticulous visual analyst. You distinguish direct observations
|
|
9
|
+
from uncertain interpretations, transcribe visible text exactly, and lower
|
|
10
|
+
confidence whenever image quality or ambiguity limits a finding.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
vision_task:
|
|
2
|
+
description: >
|
|
3
|
+
Analyze every image {image} attached to this crew run with prompt: {prompt}.
|
|
4
|
+
Identify visible objects,
|
|
5
|
+
prominent colors, names or named entities, and dates. Report only evidence
|
|
6
|
+
supported by the image. Preserve visible spelling and date formatting,
|
|
7
|
+
omit duplicates, and use an empty list when a category has no findings.
|
|
8
|
+
Assign an overall confidence percentage that reflects image quality and
|
|
9
|
+
ambiguity.
|
|
10
|
+
expected_output: >
|
|
11
|
+
A VisionSchema-compatible object containing confidence_level (0 through
|
|
12
|
+
100), items_found (strings), colors (objects with name and an optional
|
|
13
|
+
six-digit RGB hex_code), names (strings), and dates (strings). Do not
|
|
14
|
+
include prose or fields outside the schema.
|
|
15
|
+
agent: vision
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from cellenium.ai.config import AgentConfig
|
|
3
|
+
from crewai import Agent, Crew, Task
|
|
4
|
+
from cellenium.ai.agents.vision.schemas import VisionSchema
|
|
5
|
+
from cellenium.ai.agents.vision.tools.vision import VisionTool
|
|
6
|
+
from crewai.project import CrewBase, agent, crew, task
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SKILLS = Path(__file__).resolve().parent / "skills"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@CrewBase
|
|
13
|
+
class Vision(AgentConfig):
|
|
14
|
+
|
|
15
|
+
@agent
|
|
16
|
+
def vision(self) -> Agent:
|
|
17
|
+
return Agent(config=self.agents_config["vision"], llm=self.llm, skills=[SKILLS])
|
|
18
|
+
|
|
19
|
+
@task
|
|
20
|
+
def vision_task(self) -> Task:
|
|
21
|
+
return Task(config=self.tasks_config["vision_task"], output_pydantic=VisionSchema, tools=[VisionTool(llm=self.llm)])
|
|
22
|
+
|
|
23
|
+
@crew
|
|
24
|
+
def crew(self) -> Crew:
|
|
25
|
+
return Crew(agents=self.agents, tasks=self.tasks, verbose=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_vision_agent(image: list[str], prompt: str) -> VisionSchema:
|
|
29
|
+
return Vision().crew().kickoff({'image': image, 'prompt': prompt}).pydantic.model_dump()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == '__main__':
|
|
33
|
+
print(run_vision_agent(prompt='what is displayed?', image=[r'C:\Users\medvi\PycharmProjects\PythonProject\tests\files\images\img.png']))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, Field, StringConstraints
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
VisionText = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=200)]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ColorFinding(BaseModel):
|
|
9
|
+
"""A visible color and its optional six-digit RGB value."""
|
|
10
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
11
|
+
name: VisionText = Field(description="Common name of the visible color.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VisionSchema(BaseModel):
|
|
15
|
+
"""Structured, evidence-grounded observations from image analysis."""
|
|
16
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
17
|
+
confidence_level: float = Field(
|
|
18
|
+
ge=0,
|
|
19
|
+
le=100,
|
|
20
|
+
description="Overall confidence in the extracted findings, from 0 to 100.",
|
|
21
|
+
)
|
|
22
|
+
items_found: list[VisionText] = Field(
|
|
23
|
+
default_factory=list,
|
|
24
|
+
max_length=100,
|
|
25
|
+
description="Distinct visible objects or items, using short noun phrases.",
|
|
26
|
+
)
|
|
27
|
+
colors: list[ColorFinding] = Field(
|
|
28
|
+
default_factory=list,
|
|
29
|
+
max_length=50,
|
|
30
|
+
description="Colors visibly present in the image.",
|
|
31
|
+
)
|
|
32
|
+
names: list[VisionText] = Field(
|
|
33
|
+
default_factory=list,
|
|
34
|
+
max_length=100,
|
|
35
|
+
description="Names or named entities transcribed exactly as visible.",
|
|
36
|
+
)
|
|
37
|
+
dates: list[VisionText] = Field(
|
|
38
|
+
default_factory=list,
|
|
39
|
+
max_length=100,
|
|
40
|
+
description="Dates transcribed exactly as visible, without guessing a format.",
|
|
41
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: evaluator
|
|
3
|
+
description: Verify proposed vision findings against image evidence before returning them.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Evaluation Rules
|
|
7
|
+
|
|
8
|
+
1. Re-check every proposed item, color, name, and date against the image.
|
|
9
|
+
2. Remove findings that depend on assumptions, outside knowledge, or unreadable text.
|
|
10
|
+
3. Keep exact transcriptions even when spelling or date formatting is unusual.
|
|
11
|
+
4. Prefer omission over a plausible but unsupported guess.
|
|
12
|
+
5. Confirm the confidence score reflects the least reliable material finding.
|
|
13
|
+
6. Return only fields defined by the requested structured output schema.
|
|
14
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vision
|
|
3
|
+
description: Extract grounded objects, colors, visible names, and dates from attached images.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Workflow
|
|
7
|
+
|
|
8
|
+
1. Inspect each attached image before recording findings.
|
|
9
|
+
2. Record only details directly supported by visible pixels.
|
|
10
|
+
3. Use short noun phrases for objects and common names for colors.
|
|
11
|
+
4. Transcribe names and dates exactly; do not normalize uncertain text.
|
|
12
|
+
5. Remove duplicate findings across images.
|
|
13
|
+
6. Use empty lists for categories with no visible evidence.
|
|
14
|
+
|
|
15
|
+
## Confidence
|
|
16
|
+
|
|
17
|
+
Set `confidence_level` from 0 to 100 for the result as a whole. Reduce it for
|
|
18
|
+
blur, occlusion, low resolution, conflicting evidence, or uncertain text. Never
|
|
19
|
+
raise confidence merely to make the output appear complete.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import base64
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Union, Type, List, Any
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from PIL import Image
|
|
7
|
+
from crewai import LLM
|
|
8
|
+
from crewai.tools import BaseTool
|
|
9
|
+
from pydantic import BaseModel, Field, field_validator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# log = Logger(name="vision-tool")
|
|
13
|
+
|
|
14
|
+
MAX_IMAGE_RESOLUTION = 2048
|
|
15
|
+
|
|
16
|
+
LLMMessage = dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ------------------------------ #
|
|
20
|
+
# Utility Functions #
|
|
21
|
+
# ------------------------------ #
|
|
22
|
+
|
|
23
|
+
def is_base64(value: str) -> bool:
|
|
24
|
+
"""Return True if value is raw base64 or a data URI, not a file path."""
|
|
25
|
+
return value.startswith("data:image/") or not Path(value).exists()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_path(path: str) -> str:
|
|
29
|
+
"""Resolve and validate file path."""
|
|
30
|
+
resolved = Path(path).resolve()
|
|
31
|
+
if not resolved.exists():
|
|
32
|
+
# log.fire(f"Image file does not exist: {resolved}", level='debug')
|
|
33
|
+
raise ValueError(f"Image file not found: {resolved}")
|
|
34
|
+
return str(resolved)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def normalize_paths(value: Optional[Union[str, List[str]]]) -> Optional[List[str]]:
|
|
38
|
+
"""Normalize optional image paths/base64 values to list."""
|
|
39
|
+
if value is None:
|
|
40
|
+
return None
|
|
41
|
+
paths = [value] if isinstance(value, str) else value
|
|
42
|
+
for p in paths:
|
|
43
|
+
if not is_base64(p):
|
|
44
|
+
resolve_path(p)
|
|
45
|
+
return paths
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _pil_to_jpeg_b64(img: "Image.Image") -> str:
|
|
49
|
+
if img.mode in ("RGBA", "LA", "P"):
|
|
50
|
+
img = img.convert("RGB")
|
|
51
|
+
if max(img.size) > MAX_IMAGE_RESOLUTION:
|
|
52
|
+
img.thumbnail((MAX_IMAGE_RESOLUTION, MAX_IMAGE_RESOLUTION), Image.LANCZOS)
|
|
53
|
+
buf = io.BytesIO()
|
|
54
|
+
img.save(buf, format="JPEG", quality=95, subsampling=0)
|
|
55
|
+
return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def encode_image(path_or_b64: str) -> str:
|
|
59
|
+
"""Encode image as base64 data URI. Accepts a file path, raw base64, or data URI."""
|
|
60
|
+
if path_or_b64.startswith("data:image/"):
|
|
61
|
+
raw = base64.b64decode(path_or_b64.split(",", 1)[1])
|
|
62
|
+
elif is_base64(path_or_b64):
|
|
63
|
+
raw = base64.b64decode(path_or_b64)
|
|
64
|
+
else:
|
|
65
|
+
try:
|
|
66
|
+
img = Image.open(resolve_path(path_or_b64))
|
|
67
|
+
return _pil_to_jpeg_b64(img)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise ValueError(f"Cannot open image '{path_or_b64}': {e}") from e
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
img = Image.open(io.BytesIO(raw))
|
|
73
|
+
return _pil_to_jpeg_b64(img)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise ValueError(f"Failed to process image data: {e}") from e
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ------------------------------ #
|
|
79
|
+
# Pydantic Schema #
|
|
80
|
+
# ------------------------------ #
|
|
81
|
+
|
|
82
|
+
class ImagePromptSchema(BaseModel):
|
|
83
|
+
|
|
84
|
+
image_path: str = Field(..., description="Primary image path")
|
|
85
|
+
sample_image: Optional[Union[str, List[str]]] = Field(default=None, description="Optional comparison image path(s)")
|
|
86
|
+
prompt: str = Field(..., description="Prompt describing what to analyze in the image")
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
@field_validator("image_path")
|
|
90
|
+
def validate_image(cls, v: str) -> str:
|
|
91
|
+
return v if is_base64(v) else resolve_path(v)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
@field_validator("sample_image")
|
|
95
|
+
def validate_sample_images(cls, v):
|
|
96
|
+
return normalize_paths(v)
|
|
97
|
+
|
|
98
|
+
def get_all_images(self) -> List[str]:
|
|
99
|
+
"""Return all image paths used in the request."""
|
|
100
|
+
images = [self.image_path]
|
|
101
|
+
|
|
102
|
+
if self.sample_image:
|
|
103
|
+
images.extend(self.sample_image)
|
|
104
|
+
|
|
105
|
+
# log.fire(f"Vision images used: {images}", level='debug')
|
|
106
|
+
|
|
107
|
+
return images
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ------------------------------ #
|
|
111
|
+
# Tool Runtime #
|
|
112
|
+
# ------------------------------ #
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class VisionMessageBuilder:
|
|
116
|
+
|
|
117
|
+
schema: ImagePromptSchema
|
|
118
|
+
|
|
119
|
+
def build(self) -> List[LLMMessage]:
|
|
120
|
+
images = [
|
|
121
|
+
{
|
|
122
|
+
"type": "image_url",
|
|
123
|
+
"image_url": {"url": encode_image(path)},
|
|
124
|
+
}
|
|
125
|
+
for path in self.schema.get_all_images()
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
"role": "user",
|
|
131
|
+
"content": [
|
|
132
|
+
{"type": "text", "text": self.schema.prompt},
|
|
133
|
+
*images
|
|
134
|
+
],
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ------------------------------ #
|
|
140
|
+
# Vision Tool Class #
|
|
141
|
+
# ------------------------------ #
|
|
142
|
+
|
|
143
|
+
class VisionTool(BaseTool):
|
|
144
|
+
|
|
145
|
+
name: str = "Bini Vision Tool"
|
|
146
|
+
description: str = "Analyzes one or more images using a vision-capable LLM"
|
|
147
|
+
args_schema: Type[BaseModel] = ImagePromptSchema
|
|
148
|
+
|
|
149
|
+
def __init__(self, llm: LLM) -> None:
|
|
150
|
+
|
|
151
|
+
super().__init__()
|
|
152
|
+
|
|
153
|
+
if llm is None:
|
|
154
|
+
raise ValueError("BiniVisionTool requires a valid LLM instance")
|
|
155
|
+
|
|
156
|
+
self._llm = llm
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _build_messages(schema: ImagePromptSchema) -> List[LLMMessage]:
|
|
160
|
+
"""Construct messages for the vision model."""
|
|
161
|
+
builder = VisionMessageBuilder(schema=schema)
|
|
162
|
+
return builder.build()
|
|
163
|
+
|
|
164
|
+
def _run(self, **kwargs: Any) -> str:
|
|
165
|
+
schema = ImagePromptSchema(**kwargs)
|
|
166
|
+
messages = self._build_messages(schema)
|
|
167
|
+
return self._llm.call(messages=messages)
|
cellenium/ai/config.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from asyncio import Task
|
|
2
|
+
from crewai import LLM
|
|
3
|
+
from cellenium.settings import get_config
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from crewai.agents.agent_builder.base_agent import BaseAgent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentConfig:
|
|
9
|
+
|
|
10
|
+
agents: list[BaseAgent]
|
|
11
|
+
tasks: list[Task]
|
|
12
|
+
agents_config: dict = 'config/agents.yaml'
|
|
13
|
+
tasks_config: dict = 'config/tasks.yaml'
|
|
14
|
+
|
|
15
|
+
@cached_property
|
|
16
|
+
def llm(self) -> LLM:
|
|
17
|
+
return LLM(model=get_config().OPENAI_MODEL)
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import mimetypes
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def encode_image_to_data_uri(image_path: str) -> str:
|
|
7
|
+
path = Path(image_path)
|
|
8
|
+
mime_type = mimetypes.guess_type(path.name)[0] or "image/png"
|
|
9
|
+
encoded = base64.b64encode(path.read_bytes()).decode("utf-8")
|
|
10
|
+
return f"data:{mime_type};base64,{encoded}"
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bootstrap a Playwright browser/context/page.
|
|
3
|
+
|
|
4
|
+
Playwright ships and manages its own browser binaries (one-time setup:
|
|
5
|
+
`playwright install chromium`) - unlike Selenium, there is no driver-manager
|
|
6
|
+
download step to wire up here.
|
|
7
|
+
"""
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page, Playwright
|
|
11
|
+
from cellenium.settings import get_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class BrowserManager:
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
Launches a Playwright browser, context, and page for the lifetime of the instance.
|
|
19
|
+
|
|
20
|
+
:param headless: run without a visible UI window.
|
|
21
|
+
:param browser_name: one of "chromium", "firefox", "webkit".
|
|
22
|
+
:param http_credentials: optional {"username": ..., "password": ...} for HTTP basic
|
|
23
|
+
auth - Playwright only accepts this at context-creation time, so it is a
|
|
24
|
+
constructor field here rather than a runtime method.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
headless: Optional[bool] = None
|
|
28
|
+
browser_name: str = "chromium"
|
|
29
|
+
http_credentials: Optional[dict] = None
|
|
30
|
+
playwright: Playwright = field(init=False, repr=False)
|
|
31
|
+
browser: Browser = field(init=False, repr=False)
|
|
32
|
+
context: BrowserContext = field(init=False, repr=False)
|
|
33
|
+
page: Page = field(init=False, repr=False)
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
if self.headless is None:
|
|
37
|
+
self.headless = get_config().TEST_HEADLESS
|
|
38
|
+
self.playwright = sync_playwright().start()
|
|
39
|
+
launcher = getattr(self.playwright, self.browser_name)
|
|
40
|
+
self.browser = launcher.launch(headless=self.headless)
|
|
41
|
+
self.context = self.browser.new_context(http_credentials=self.http_credentials)
|
|
42
|
+
self.page = self.context.new_page()
|
|
43
|
+
|
|
44
|
+
def teardown(self) -> None:
|
|
45
|
+
self.page.close()
|
|
46
|
+
self.context.close()
|
|
47
|
+
self.browser.close()
|
|
48
|
+
self.playwright.stop()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal, Optional
|
|
5
|
+
from playwright.sync_api import Locator
|
|
6
|
+
from cellenium.engine.manager import BrowserManager
|
|
7
|
+
from cellenium.functions.locators import get_entry, get_locator
|
|
8
|
+
from cellenium.functions.logger import Logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SCREENSHOTS = Path("data") / "screenshots" # relative to the working directory, not the installed package
|
|
12
|
+
|
|
13
|
+
log = Logger(name='browser-manager')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PageEngine(BrowserManager):
|
|
18
|
+
|
|
19
|
+
screen: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
def get_web(self, url: str) -> None:
|
|
22
|
+
self.page.goto(url)
|
|
23
|
+
log.fire(message=f'web opened: {url} using: {self.screen} screen')
|
|
24
|
+
|
|
25
|
+
def get_element(self, name: str, timeout: int = 10_000) -> Locator:
|
|
26
|
+
locator = get_locator(self.page, self.screen, name)
|
|
27
|
+
locator.wait_for(state="attached", timeout=timeout)
|
|
28
|
+
log.fire(message=f'element used: {name}')
|
|
29
|
+
return locator
|
|
30
|
+
|
|
31
|
+
def get_dynamic_element(self, attribute: str, name: str) -> Locator:
|
|
32
|
+
# explanation ............ //*[contains(@<attribute>, <name>)]
|
|
33
|
+
# example ................ //*[contains(@name, "btnK")]
|
|
34
|
+
entry = get_entry(self.screen, name)
|
|
35
|
+
path = f"//*[contains(@{attribute}, '{entry['value']}')]"
|
|
36
|
+
return self.page.locator(f"xpath={path}")
|
|
37
|
+
|
|
38
|
+
def wait_for_element(self, name: str, timeout: int = 5_000) -> Locator:
|
|
39
|
+
locator = get_locator(self.page, self.screen, name)
|
|
40
|
+
locator.wait_for(state="visible", timeout=timeout)
|
|
41
|
+
log.fire(message=f'waiting for element: {name}')
|
|
42
|
+
return locator
|
|
43
|
+
|
|
44
|
+
def get_screenshot(self, name: Optional[str] = None) -> str:
|
|
45
|
+
SCREENSHOTS.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
file_name = f"{name or uuid.uuid4()}.png"
|
|
47
|
+
path = SCREENSHOTS / file_name
|
|
48
|
+
self.page.screenshot(path=str(path))
|
|
49
|
+
return str(path)
|
|
50
|
+
|
|
51
|
+
def dropdown(self, name: str, label: str) -> None:
|
|
52
|
+
self.get_element(name).select_option(label=label)
|
|
53
|
+
|
|
54
|
+
def count_elements(self, name: str, tag: str) -> int:
|
|
55
|
+
count = self.get_element(name).locator(tag).count()
|
|
56
|
+
print(f"number of rows in this page is: {count}")
|
|
57
|
+
return count
|
|
58
|
+
|
|
59
|
+
def count_rows(self, name: str, structure: str) -> int:
|
|
60
|
+
return self.get_element(name).locator(structure).count()
|
|
61
|
+
|
|
62
|
+
def press_keyboard_key(self, key: str) -> None:
|
|
63
|
+
self.page.keyboard.press(f"Control+{key}")
|
|
64
|
+
|
|
65
|
+
def scroll_page(self, direction: Literal['up', 'down'], px: int) -> None:
|
|
66
|
+
match direction:
|
|
67
|
+
case "up":
|
|
68
|
+
self.page.mouse.wheel(0, -px)
|
|
69
|
+
case "down":
|
|
70
|
+
self.page.mouse.wheel(0, px)
|
|
71
|
+
|
|
72
|
+
def switch_to_new_tab(self, url: str) -> None:
|
|
73
|
+
new_page = self.context.new_page()
|
|
74
|
+
new_page.goto(url)
|
|
75
|
+
log.fire(message=f'url switched to {url}')
|
|
76
|
+
|
|
77
|
+
def switch_to_main_tab(self) -> None:
|
|
78
|
+
self.page = self.context.pages[0]
|
|
79
|
+
self.page.bring_to_front()
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from playwright.sync_api import Locator, Page
|
|
4
|
+
|
|
5
|
+
from cellenium.functions.sheets import fetch_locators
|
|
6
|
+
|
|
7
|
+
# Selenium's find_element silently returns the first match; Playwright locators are
|
|
8
|
+
# strict-mode by default and raise on multiple matches, so each strategy here resolves
|
|
9
|
+
# to the first match (via .first) to keep that same "resolve to one element" semantics.
|
|
10
|
+
LOCATOR_MAP: dict[str, Callable[[Page, str], Locator]] = {
|
|
11
|
+
"NAME": lambda page, v: page.locator(f'[name="{v}"]').first,
|
|
12
|
+
"XPATH": lambda page, v: page.locator(f"xpath={v}").first,
|
|
13
|
+
"ID": lambda page, v: page.locator(f"#{v}").first,
|
|
14
|
+
"CSS": lambda page, v: page.locator(v).first,
|
|
15
|
+
"CLASS_NAME": lambda page, v: page.locator(f".{v}").first,
|
|
16
|
+
"LINK_TEXT": lambda page, v: page.get_by_text(v, exact=True).first,
|
|
17
|
+
"TAG_NAME": lambda page, v: page.locator(v).first,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_entry(screen: str | None, name: str) -> dict[str, str]:
|
|
22
|
+
"""Look up a raw POM entry from the worksheet selected by screen."""
|
|
23
|
+
entries = fetch_locators(screen)
|
|
24
|
+
if name not in entries:
|
|
25
|
+
raise KeyError(f"No locator named {name!r} on screen {screen!r}")
|
|
26
|
+
|
|
27
|
+
return entries[name]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_locator(page: Page, screen: str | None, name: str) -> Locator:
|
|
31
|
+
"""Resolve a Google Sheets POM entry to a Playwright Locator."""
|
|
32
|
+
entry = get_entry(screen, name)
|
|
33
|
+
strategy = entry["type"]
|
|
34
|
+
try:
|
|
35
|
+
builder = LOCATOR_MAP[strategy]
|
|
36
|
+
except KeyError:
|
|
37
|
+
raise ValueError(f"Unsupported locator strategy {strategy!r} for {name!r} on screen {screen!r}")
|
|
38
|
+
return builder(page, entry["value"])
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import logfire
|
|
6
|
+
|
|
7
|
+
from cellenium.settings import get_config
|
|
8
|
+
|
|
9
|
+
LogLevel = Literal["info", "error", "debug", "warning", "fatal"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Logger:
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
|
|
17
|
+
@cached_property
|
|
18
|
+
def config(self):
|
|
19
|
+
# Configured on first log call so module-level loggers don't load settings at import time.
|
|
20
|
+
return logfire.configure(
|
|
21
|
+
token=get_config().LOGFIRE_TOKEN,
|
|
22
|
+
send_to_logfire="if-token-present",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def fire(self, message: str, level: LogLevel | None = 'info', **attributes: object) -> None:
|
|
26
|
+
with self.config.span(self.name):
|
|
27
|
+
match level:
|
|
28
|
+
case "info":
|
|
29
|
+
self.config.info(message, **attributes)
|
|
30
|
+
case "error":
|
|
31
|
+
self.config.error(message, **attributes)
|
|
32
|
+
case "debug":
|
|
33
|
+
self.config.debug(message, **attributes)
|
|
34
|
+
case "warning":
|
|
35
|
+
self.config.warning(message, **attributes)
|
|
36
|
+
case "fatal":
|
|
37
|
+
self.config.fatal(message, **attributes)
|
|
38
|
+
case _:
|
|
39
|
+
self.config.info(message, **attributes)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
logger = Logger(name="app")
|
|
44
|
+
logger.fire(message="hi")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read Page Object Model locators directly from Google Sheets.
|
|
3
|
+
Uses gspread with a service-account credentials.json (no "anyone with the
|
|
4
|
+
link" sharing required) - the sheet just needs to be shared with the service
|
|
5
|
+
account's client_email (see credentials.json) as a Viewer.
|
|
6
|
+
|
|
7
|
+
Each worksheet (tab) in the spreadsheet is one screen. Callers select the
|
|
8
|
+
worksheet by passing the same screen name used by ``PageEngine``.
|
|
9
|
+
|
|
10
|
+
Sheet columns (per the QA team's POM sheet):
|
|
11
|
+
name - identifier used to look up the locator in code
|
|
12
|
+
locator - the strategy, e.g. NAME / XPATH / ID / CSS
|
|
13
|
+
type - the actual locator value (confusingly named, but matches the sheet)
|
|
14
|
+
actions - optional
|
|
15
|
+
comments - optional
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import gspread
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
21
|
+
from cellenium.functions.logger import Logger
|
|
22
|
+
from cellenium.settings import get_config
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
log = Logger('sheets-logic')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_worksheet(worksheet: gspread.Worksheet) -> dict[str, dict[str, str]]:
|
|
29
|
+
|
|
30
|
+
rows = worksheet.get_all_values()
|
|
31
|
+
|
|
32
|
+
if not rows:
|
|
33
|
+
log.fire(message='no rows were found', level='debug')
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
header = [h.strip().lower() for h in rows[0]]
|
|
37
|
+
locators: dict[str, dict[str, str]] = {}
|
|
38
|
+
|
|
39
|
+
for row in rows[1:]:
|
|
40
|
+
record = dict(zip(header, row))
|
|
41
|
+
name = record.get("name", "").strip()
|
|
42
|
+
if not name:
|
|
43
|
+
continue
|
|
44
|
+
locators[name] = {
|
|
45
|
+
"type": record.get("locator", "").strip().upper(),
|
|
46
|
+
"value": record.get("type", "").strip(),
|
|
47
|
+
"actions": record.get("actions", "").strip(),
|
|
48
|
+
"comments": record.get("comments", "").strip(),
|
|
49
|
+
}
|
|
50
|
+
return locators
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fetch_locators(screen: Optional[str] = None, credentials_path: Optional[Path] = None) -> dict[str, dict]:
|
|
54
|
+
"""Read all locator rows from one worksheet selected by screen name."""
|
|
55
|
+
if not screen:
|
|
56
|
+
message = 'screen is required to select a Google Sheets worksheet'
|
|
57
|
+
log.fire(message=message, level='error')
|
|
58
|
+
raise ValueError(message)
|
|
59
|
+
|
|
60
|
+
config = get_config()
|
|
61
|
+
gc = gspread.service_account(filename=str(credentials_path or config.CREDENTIALS_JSON))
|
|
62
|
+
spreadsheet = gc.open_by_url(config.GOOGLE_SHEETS)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
worksheet = spreadsheet.worksheet(screen)
|
|
66
|
+
|
|
67
|
+
except gspread.WorksheetNotFound as e:
|
|
68
|
+
log.fire(f"No worksheet named {screen!r} in the configured Google Sheet", level='error')
|
|
69
|
+
raise ValueError from e
|
|
70
|
+
|
|
71
|
+
return _parse_worksheet(worksheet)
|
cellenium/settings.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from functools import cache
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Settings(BaseSettings):
|
|
8
|
+
|
|
9
|
+
# Relative paths resolve against the current working directory, i.e. the project using cellenium.
|
|
10
|
+
model_config = SettingsConfigDict(env_file=".env", extra="allow")
|
|
11
|
+
|
|
12
|
+
GOOGLE_SHEETS: str = Field(..., description='')
|
|
13
|
+
GOOGLE_SHEET_API_KEY: str = Field(..., description='')
|
|
14
|
+
GOOGLE_SHEET_EMAIL: str = Field(..., description='')
|
|
15
|
+
GOOGLE_SHEET_ID: str = Field(..., description='')
|
|
16
|
+
OPENAI_MODEL: str = Field(..., description='')
|
|
17
|
+
OPENAI_API_KEY: str | None = Field(None, description='')
|
|
18
|
+
LOGFIRE_TOKEN: str | None = Field(None, description='')
|
|
19
|
+
TEST_HEADLESS: bool = Field(True, description='')
|
|
20
|
+
CREDENTIALS_JSON: Path = Field(Path("credentials.json"), description='google json credentials for accessing the sheet')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@cache
|
|
24
|
+
def get_config() -> Settings:
|
|
25
|
+
"""Load settings on first use so importing cellenium never requires a configured environment."""
|
|
26
|
+
return Settings()
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cellenium
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Playwright browser automation with a Google Sheets Page Object Model and CrewAI vision analysis
|
|
5
|
+
Project-URL: Homepage, https://github.com/MedviJenka/cellenium-playwright
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Requires-Dist: crewai>=1.15.23
|
|
9
|
+
Requires-Dist: fastapi>=0.142.2
|
|
10
|
+
Requires-Dist: gspread>=6.2.1
|
|
11
|
+
Requires-Dist: importlib-metadata>=8.0.0
|
|
12
|
+
Requires-Dist: logfire>=3.5.1
|
|
13
|
+
Requires-Dist: oauth2client>=4.1.3
|
|
14
|
+
Requires-Dist: pillow>=12.3.0
|
|
15
|
+
Requires-Dist: playwright>=1.63.0
|
|
16
|
+
Requires-Dist: pydantic-settings>=2.15.0
|
|
17
|
+
Requires-Dist: uvicorn>=0.54.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Cellenium Playwright
|
|
21
|
+
|
|
22
|
+
Cellenium Playwright is an experimental Python browser-automation toolkit built on [Playwright](https://playwright.dev/python/). It provides browser lifecycle management, a Google-Sheets-backed Page Object Model (POM), screenshot capture, and components for structured visual analysis with CrewAI.
|
|
23
|
+
|
|
24
|
+
The repository also contains **TestFlow**, a set of Claude Code skills that turns product requirements and technical specifications into traceable pytest tests, validated UI locators, execution evidence, and a test-quality verdict.
|
|
25
|
+
|
|
26
|
+
> **Status:** active development. The browser and locator layers are usable, but the checked-in vision test is still a scaffold and does not currently pass. Install it from PyPI with `pip install cellenium` (or `uv add cellenium`), then run `playwright install chromium`. Configuration (`.env`, `credentials.json`) is read from your project's working directory.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- Synchronous Chromium, Firefox, or WebKit sessions through Playwright.
|
|
31
|
+
- Reusable `PageEngine` helpers for navigation, locators, screenshots, scrolling, dropdowns, and tabs.
|
|
32
|
+
- Page Object Model data read directly from Google Sheets at runtime.
|
|
33
|
+
- Locator strategies for name, XPath, ID, CSS, class name, link text, and tag name.
|
|
34
|
+
- Structured image-analysis components built with CrewAI and Pydantic.
|
|
35
|
+
- Parallel pytest execution in CI on Python 3.13 and 3.14.
|
|
36
|
+
- Optional TestFlow skills for end-to-end pytest test generation and evaluation.
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- Python 3.13 or 3.14
|
|
41
|
+
- [`uv`](https://docs.astral.sh/uv/)
|
|
42
|
+
- A Playwright-supported browser
|
|
43
|
+
- A Google service account for resolving locators
|
|
44
|
+
- An OpenAI-compatible model and API key when using the vision components
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
Clone the repository and install the locked dependencies:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
git clone https://github.com/MedviJenka/cellenium-playwright.git
|
|
52
|
+
cd cellenium-playwright
|
|
53
|
+
uv sync --frozen
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Install Chromium:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
uv run playwright install chromium
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
On Ubuntu or another Debian-based CI runner, install Chromium and its system dependencies together:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
uv run playwright install --with-deps chromium
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
Create `.env` in the repository root. Optional values are labeled below:
|
|
71
|
+
|
|
72
|
+
```dotenv
|
|
73
|
+
GOOGLE_SHEETS=<spreadsheet-id-or-edit-url>
|
|
74
|
+
GOOGLE_SHEET_API_KEY=<google-api-key>
|
|
75
|
+
GOOGLE_SHEET_EMAIL=<service-account-email>
|
|
76
|
+
GOOGLE_SHEET_ID=<spreadsheet-id>
|
|
77
|
+
OPENAI_MODEL=<model-name>
|
|
78
|
+
OPENAI_API_KEY=<optional-api-key>
|
|
79
|
+
LOGFIRE_TOKEN=<optional-logfire-token>
|
|
80
|
+
TEST_HEADLESS=true
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`GOOGLE_SHEETS` selects the locator spreadsheet. When `LOGFIRE_TOKEN` is omitted, logging remains local and no data is sent to Logfire.
|
|
84
|
+
|
|
85
|
+
Place the Google service-account document at:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
credentials.json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Share the locator spreadsheet with the service account's `client_email` as a viewer. Never commit `.env` or `credentials.json`; both are ignored by Git.
|
|
92
|
+
|
|
93
|
+
## Google Sheets Page Object Model
|
|
94
|
+
|
|
95
|
+
Each worksheet tab represents one screen. The first row must contain these columns:
|
|
96
|
+
|
|
97
|
+
| Column | Purpose |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `name` | Logical element name used by tests |
|
|
100
|
+
| `locator` | Locator strategy such as `NAME`, `XPATH`, `ID`, or `CSS` |
|
|
101
|
+
| `type` | Locator value; this name is retained for compatibility with the source sheet |
|
|
102
|
+
| `actions` | Optional metadata |
|
|
103
|
+
| `comments` | Optional notes |
|
|
104
|
+
|
|
105
|
+
Example worksheet:
|
|
106
|
+
|
|
107
|
+
| name | locator | type | actions | comments |
|
|
108
|
+
|---|---|---|---|---|
|
|
109
|
+
| search | NAME | q | fill | Google search field |
|
|
110
|
+
| button | NAME | btnK | click | Search button |
|
|
111
|
+
|
|
112
|
+
`PageEngine(screen="Google")` reads locator rows directly from the worksheet tab
|
|
113
|
+
named `Google`. No generated JSON mapping or synchronization step is required.
|
|
114
|
+
|
|
115
|
+
## Browser automation
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from src.core.engine.page_engine import PageEngine
|
|
119
|
+
|
|
120
|
+
engine = PageEngine(screen="Google", headless=True)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
engine.get_web("https://www.google.com")
|
|
124
|
+
engine.get_element("search").fill("cats")
|
|
125
|
+
engine.get_element("button").click()
|
|
126
|
+
screenshot = engine.get_screenshot("google-results")
|
|
127
|
+
print(screenshot)
|
|
128
|
+
finally:
|
|
129
|
+
engine.teardown()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`screen="Google"` selects the worksheet tab named `Google`. With multiple tabs, pass the page's tab name when constructing `PageEngine` (for example, `Google`, `Heroku`, or `ST`). Missing worksheets, missing element names, and unsupported locator strategies raise explicit exceptions.
|
|
133
|
+
|
|
134
|
+
## Tests and linting
|
|
135
|
+
|
|
136
|
+
Run the test suite:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
uv run --frozen pytest src/tests tests -v -n auto --dist loadscope
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`src/tests/test_google_search.py` exercises the live browser flow. The unit regressions in `tests/` cover screenshot persistence and direct worksheet selection without contacting external services.
|
|
143
|
+
|
|
144
|
+
Run the blocking CI lint checks:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
uvx --from flake8==7.4.1 flake8 main.py settings.py src \
|
|
148
|
+
--count --select=E9,F63,F7,F82 --show-source --statistics
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The GitHub Actions workflow in `.github/workflows/python-package.yml` performs locked dependency installation, installs Chromium, runs lint checks, and executes the tests under Xvfb for Python 3.13 and 3.14.
|
|
152
|
+
|
|
153
|
+
## TestFlow Claude Code skills
|
|
154
|
+
|
|
155
|
+
TestFlow separates pytest creation into focused stages:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
PRD → test specification → UI locators → pytest writer → executor → evaluator
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
| Order | Skill | Purpose |
|
|
162
|
+
|---:|---|---|
|
|
163
|
+
| Optional | `web` | Explore a live application in a real browser and produce reproducible bug evidence |
|
|
164
|
+
| 1 | `prd-reader` | Extract explicit, derived, and unknown test requirements from the PRD |
|
|
165
|
+
| 2 | `test-spec` | Design traceable pytest cases, boundaries, fixtures, and execution prerequisites |
|
|
166
|
+
| 3 | `locator-writer` | For UI cases, validate stable Playwright selectors and upsert elements into Google Sheets |
|
|
167
|
+
| 4 | `writer` | Implement deterministic, behavior-focused pytest tests |
|
|
168
|
+
| 5 | `executor` | Run collection, targeted tests, related tests, and classify failures |
|
|
169
|
+
| 6 | `evaluator` | Evaluate traceability, regression sensitivity, coverage quality, and execution evidence |
|
|
170
|
+
|
|
171
|
+
`artifact` orchestrates the complete pipeline and repeats only the affected downstream stages when evaluation finds a defect.
|
|
172
|
+
|
|
173
|
+
`web` is a report-only exploratory entry point. Verified findings become regression candidates for `test-spec`; it does not modify application code.
|
|
174
|
+
|
|
175
|
+
Validate the Claude Code plugin metadata:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
claude plugin validate .
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Run Claude Code from the repository and invoke either the complete pipeline or an individual stage:
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
/testflow:web
|
|
185
|
+
/testflow:artifact
|
|
186
|
+
/testflow:prd-reader
|
|
187
|
+
/testflow:test-spec
|
|
188
|
+
/testflow:locator-writer
|
|
189
|
+
/testflow:writer
|
|
190
|
+
/testflow:executor
|
|
191
|
+
/testflow:evaluator
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The skills are manual-only. The locator writer previews changes before replacing existing Google Sheet selectors and verifies selectors against the live UI.
|
|
195
|
+
|
|
196
|
+
The separate checklist skills live under `.claude/skills/checklist/`: `before-startup` (system ready to build tests), `cruising` (everything in place, critical errors and potential bugs), `landing` (final production readiness), and the standalone `fordec` emergency decision skill.
|
|
197
|
+
|
|
198
|
+
## Project layout
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
.github/workflows/ GitHub Actions CI
|
|
202
|
+
.claude/skills/testflow/ Pytest TestFlow skill definitions
|
|
203
|
+
.claude/skills/checklist/ Checklist skills (before-startup, cruising, landing, fordec)
|
|
204
|
+
.claude/commands/testflow/ TestFlow slash-command entries
|
|
205
|
+
.claude-plugin/plugin.json Claude Code plugin metadata
|
|
206
|
+
src/core/engine/ Playwright browser and page engines
|
|
207
|
+
src/core/functions/ Locator resolution and direct Google Sheets access
|
|
208
|
+
src/core/ai/ CrewAI configuration and vision components
|
|
209
|
+
src/tests/ Browser test scenarios
|
|
210
|
+
settings.py Environment-backed runtime configuration
|
|
211
|
+
pyproject.toml Python requirements and dependencies
|
|
212
|
+
uv.lock Reproducible dependency lockfile
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Known limitations
|
|
216
|
+
|
|
217
|
+
- The checked-in vision test uses a placeholder `VisionAssertion` without a `run()` implementation.
|
|
218
|
+
- The sample end-to-end test depends on Google and is not hermetic.
|
|
219
|
+
- Configuration currently requires fields that are reserved but not consumed by every component.
|
|
220
|
+
- The project does not yet define a wheel/sdist build or publishing workflow.
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
Licensed under the [ISC License](LICENSE). Copyright © 2026 Jenia Petrusenko.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
cellenium/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
cellenium/settings.py,sha256=6r5flnKTZ0Sr16Y_SIuPXQEGr-bWtR9N6UirColrEiQ,1384
|
|
3
|
+
cellenium/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
cellenium/ai/config.py,sha256=xpT5HDa0A1ff_H-UyZAVWHb_t2WhLJG9Ysx5NVpTTbk,470
|
|
5
|
+
cellenium/ai/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
cellenium/ai/agents/vision/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
cellenium/ai/agents/vision/crew.py,sha256=-LKXwVYVQjCrnhbfVoTx39l1P6oDcAPhLsA2Xb0d6Ps,1158
|
|
8
|
+
cellenium/ai/agents/vision/schemas.py,sha256=KZPZYlPzC95GPyEfz-qNYpC-HMLs8E0INIoKzf70ixY,1505
|
|
9
|
+
cellenium/ai/agents/vision/config/agents.yaml,sha256=ukcIjykZ8_M8NDQwsByUgU2L7UPdHK23x3c2YhzGMLU,434
|
|
10
|
+
cellenium/ai/agents/vision/config/tasks.yaml,sha256=oV4aA1WxgjEzeOwvI73mqCM5XmlV_stZ9Li5vi2S2S8,788
|
|
11
|
+
cellenium/ai/agents/vision/skills/evaluator/SKILL.md,sha256=icUO8l-eum2qAkb7hXnRJ817BDedv3WMXMHRPMpvgVs,601
|
|
12
|
+
cellenium/ai/agents/vision/skills/vision/SKILL.md,sha256=m75iOqKpqAguUolohZ9cKnifzqElej6NVdyvgvYLl2M,744
|
|
13
|
+
cellenium/ai/agents/vision/tools/vision.py,sha256=fx9Pfwh-VmItyAcdiAum_9ROWH8mlDwgJ9WT68qd9xg,5319
|
|
14
|
+
cellenium/ai/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
cellenium/ai/tools/image_encoder.py,sha256=b4M5W_FwJ2iH8ZwS4AmCBbXXpZ-fM6YlYkapzHDLUoY,330
|
|
16
|
+
cellenium/engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
cellenium/engine/manager.py,sha256=Fi7lTVXtkI8Lht-tAJuYr1gfraEEBo2ExmgIpfi2-fg,1892
|
|
18
|
+
cellenium/engine/page_engine.py,sha256=iwaDj9R5IjZym86nz9uh9yONqAj8h2WIocQ2gQVXrM0,3032
|
|
19
|
+
cellenium/functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
cellenium/functions/locators.py,sha256=hoCfO8zg9dljeK1squKyt-sH5dKjZaRC0jtKZcivkBs,1648
|
|
21
|
+
cellenium/functions/logger.py,sha256=68uZtamrCtCqmHMSwcXcDUPiz4HkjhXz0rbEYanFzU0,1344
|
|
22
|
+
cellenium/functions/sheets.py,sha256=UYQUmZYrTOZlWN1oapzLwOnQ1ZxQAHiTu24ggCF1UL0,2545
|
|
23
|
+
cellenium-0.1.0.dist-info/METADATA,sha256=NN_aezscdzaya0FhxMjWVRKJ9dISAXHF7jeM4bdtaKk,8990
|
|
24
|
+
cellenium-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
25
|
+
cellenium-0.1.0.dist-info/licenses/LICENSE,sha256=TsSLLu43bQqPdjqazLS4p4OpWvYomR6MYlLs10is-P4,754
|
|
26
|
+
cellenium-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jenia Petrusenko
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|