petekit 0.1.0a0__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.
- petekit-0.1.0a0/LICENSE +22 -0
- petekit-0.1.0a0/PKG-INFO +16 -0
- petekit-0.1.0a0/README.md +155 -0
- petekit-0.1.0a0/petekit/image/__init__.py +9 -0
- petekit-0.1.0a0/petekit/image/camera_observer.py +96 -0
- petekit-0.1.0a0/petekit/image/image_buffer_manager.py +65 -0
- petekit-0.1.0a0/petekit/image/multi_camera_observer.py +133 -0
- petekit-0.1.0a0/petekit/image/screenshooter.py +40 -0
- petekit-0.1.0a0/petekit/petekit.egg-info/PKG-INFO +16 -0
- petekit-0.1.0a0/petekit/petekit.egg-info/SOURCES.txt +37 -0
- petekit-0.1.0a0/petekit/petekit.egg-info/dependency_links.txt +1 -0
- petekit-0.1.0a0/petekit/petekit.egg-info/requires.txt +11 -0
- petekit-0.1.0a0/petekit/petekit.egg-info/top_level.txt +5 -0
- petekit-0.1.0a0/petekit/stream/__init__.py +22 -0
- petekit-0.1.0a0/petekit/stream/basher.py +235 -0
- petekit-0.1.0a0/petekit/stream/connector.py +156 -0
- petekit-0.1.0a0/petekit/stream/pattern_matchers/fsa_stream.py +45 -0
- petekit-0.1.0a0/petekit/stream/pattern_matchers/grammar_stream.py +38 -0
- petekit-0.1.0a0/petekit/stream/pattern_matchers/heavy_hitter_stream.py +48 -0
- petekit-0.1.0a0/petekit/stream/pattern_matchers/multiline_regex_stream.py +37 -0
- petekit-0.1.0a0/petekit/stream/pattern_matchers/regex_stream.py +155 -0
- petekit-0.1.0a0/petekit/stream/sandboxed_basher.py +128 -0
- petekit-0.1.0a0/petekit/stream/stream_buffer_manager.py +234 -0
- petekit-0.1.0a0/petekit/stream/stream_forwarder.py +33 -0
- petekit-0.1.0a0/petekit/stream/stream_observer.py +172 -0
- petekit-0.1.0a0/petekit/text/__init__.py +9 -0
- petekit-0.1.0a0/petekit/text/buffer_manager.py +465 -0
- petekit-0.1.0a0/petekit/text/text_editor.py +186 -0
- petekit-0.1.0a0/petekit/text/web_navigator.py +272 -0
- petekit-0.1.0a0/petekit/tools/__init__.py +5 -0
- petekit-0.1.0a0/petekit/tools/psh.py +304 -0
- petekit-0.1.0a0/petekit/utils/__init__.py +5 -0
- petekit-0.1.0a0/petekit/utils/camera_driver/__init__.py +9 -0
- petekit-0.1.0a0/petekit/utils/camera_driver/camera_driver.py +50 -0
- petekit-0.1.0a0/petekit/utils/camera_driver/cv2_camera_driver.py +145 -0
- petekit-0.1.0a0/petekit/utils/camera_driver/v4l2_camera_driver.py +147 -0
- petekit-0.1.0a0/petekit/utils/three_merge.py +246 -0
- petekit-0.1.0a0/pyproject.toml +23 -0
- petekit-0.1.0a0/setup.cfg +4 -0
petekit-0.1.0a0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
---------
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Schäfer List Systems GmbH
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
petekit-0.1.0a0/PKG-INFO
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: petekit
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: PeteOS Kit — agentic building blocks for streams, text, and images
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: peteos
|
|
8
|
+
Requires-Dist: diff-match-patch
|
|
9
|
+
Provides-Extra: image
|
|
10
|
+
Requires-Dist: opencv-python>=4.8.0; extra == "image"
|
|
11
|
+
Requires-Dist: numpy>=1.24.0; extra == "image"
|
|
12
|
+
Requires-Dist: mss; extra == "image"
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# PeteOS Kit
|
|
2
|
+
|
|
3
|
+
A collection of PeteOS agentic building blocks for streams, text, and images.
|
|
4
|
+
|
|
5
|
+
PeteOS Kit provides ready-to-use agentic objects you can inherit, compose, and deploy without building from scratch.
|
|
6
|
+
|
|
7
|
+
## Subpackages
|
|
8
|
+
|
|
9
|
+
- **`peteos_kit.stream`** — stream buffering, pattern matching, shell execution, and network connections
|
|
10
|
+
- **`peteos_kit.text`** — buffer management, text editing, and web navigation
|
|
11
|
+
- **`peteos_kit.image`** — image buffers, camera observation, and screen capture
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from peteos import AgenticObject
|
|
17
|
+
from peteos_kit import RegexStreamObserver, StreamForwarder
|
|
18
|
+
|
|
19
|
+
# Create a composed agent
|
|
20
|
+
class LogMonitor(RegexStreamObserver, StreamForwarder, AgenticObject):
|
|
21
|
+
"""You monitor log streams for patterns and forward interesting entries."""
|
|
22
|
+
|
|
23
|
+
# Use it
|
|
24
|
+
monitor = LogMonitor()
|
|
25
|
+
result = await monitor.invoke_agent("Set up a pattern for ERROR messages in my syslog stream.")
|
|
26
|
+
print(result)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Key Components
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
---
|
|
33
|
+
title: PeteOS Kit — Agentic Class Inheritance Hierarchy
|
|
34
|
+
---
|
|
35
|
+
classDiagram
|
|
36
|
+
direction TB
|
|
37
|
+
|
|
38
|
+
class BufferManager {
|
|
39
|
+
<<AgenticObject>>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class StreamBufferManager {
|
|
43
|
+
<<AgenticObject>>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class StreamObserver {
|
|
47
|
+
<<AgenticObject>>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class StreamForwarder {
|
|
51
|
+
<<AgenticObject>>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class RegexStreamObserver {
|
|
55
|
+
<<AgenticObject>>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class Basher {
|
|
59
|
+
<<AgenticObject>>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class SandboxedBasher {
|
|
63
|
+
<<AgenticObject>>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class Connector {
|
|
67
|
+
<<AgenticObject>>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class TextEditor {
|
|
71
|
+
<<AgenticObject>>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class WebNavigator {
|
|
75
|
+
<<AgenticObject>>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class NumPyBufferManager {
|
|
79
|
+
<<AgenticObject>>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class ImageBufferManager {
|
|
83
|
+
<<AgenticObject>>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class CameraObserver {
|
|
87
|
+
<<AgenticObject>>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class Screenshooter {
|
|
91
|
+
<<AgenticObject>>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
%% Base hierarchy
|
|
95
|
+
BufferManager <|-- StreamBufferManager
|
|
96
|
+
StreamBufferManager <|-- StreamObserver
|
|
97
|
+
|
|
98
|
+
%% Action & shell agents on streams
|
|
99
|
+
StreamBufferManager <|-- StreamForwarder
|
|
100
|
+
StreamBufferManager <|-- Basher
|
|
101
|
+
StreamBufferManager <|-- Connector
|
|
102
|
+
Basher <|-- SandboxedBasher
|
|
103
|
+
|
|
104
|
+
%% Pattern matchers
|
|
105
|
+
StreamObserver <|-- RegexStreamObserver
|
|
106
|
+
|
|
107
|
+
%% Text agents
|
|
108
|
+
BufferManager <|-- TextEditor
|
|
109
|
+
BufferManager <|-- WebNavigator
|
|
110
|
+
|
|
111
|
+
%% Image agents
|
|
112
|
+
NumPyBufferManager <|-- ImageBufferManager
|
|
113
|
+
ImageBufferManager <|-- CameraObserver
|
|
114
|
+
ImageBufferManager <|-- Screenshooter
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- **`BufferManager`** — in-memory text buffers with search, diff, and edit
|
|
118
|
+
- **`StreamBufferManager`** — rolling, append-only stream buffers with rule-based routing
|
|
119
|
+
- **`StreamObserver`** — observes streams and surfaces unexpected entries to the agent
|
|
120
|
+
- **`RegexStreamObserver`** — regex-based pattern matchers as conditions for stream rules
|
|
121
|
+
- **`TextEditor`** — file editing backed by a multi-file buffer
|
|
122
|
+
- **`WebNavigator`** — web navigation backed by a buffer
|
|
123
|
+
- **`ImageBufferManager`** — numpy-backed image buffers
|
|
124
|
+
- **`CameraObserver`** — multi-camera observation with buffer management
|
|
125
|
+
- **`Screenshooter`** — screen capture into image buffers
|
|
126
|
+
- **`Basher`** / **`SandboxedBasher`** — shell execution with buffer integration
|
|
127
|
+
|
|
128
|
+
## Installation
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
python -m venv .venv
|
|
132
|
+
source .venv/bin/activate
|
|
133
|
+
pip install peteos_kit
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
For image processing support:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pip install peteos_kit[image]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
PeteOS Kit requires a working `peteos.json` in your project root or config directory. See the [PeteOS configuration docs](https://docs.peteos.ai/0.3.x/docs/config/) for details.
|
|
145
|
+
|
|
146
|
+
## Resources
|
|
147
|
+
|
|
148
|
+
- [PeteOS](https://peteos.ai) — the underlying framework
|
|
149
|
+
- [PeteOS on GitHub](https://github.com/Schafer-List-Systems/peteos) — PeteOS framework source
|
|
150
|
+
- [PeteOS Docs](https://docs.peteos.ai/0.3.x/docs/) — concepts, reference, and best practices
|
|
151
|
+
- [PeteOS Kit on GitHub](https://github.com/Schafer-List-Systems/peteos_kit) — source and issues
|
|
152
|
+
|
|
153
|
+
## Licensing
|
|
154
|
+
|
|
155
|
+
This project is MIT-licensed. See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""CameraObserver - webcam access via a CameraDriver, backed by ImageBufferManager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
|
|
7
|
+
from peteos import AgenticObject
|
|
8
|
+
from peteos.oap.decorators import tool
|
|
9
|
+
|
|
10
|
+
from petekit.utils.camera_driver import V4L2CameraDriver
|
|
11
|
+
from .image_buffer_manager import ImageBufferManager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CameraObserver(ImageBufferManager, AgenticObject):
|
|
15
|
+
"""You are an observer with access to cameras.
|
|
16
|
+
|
|
17
|
+
Only one camera can be open at a time. grab_image stores frames in ImageBufferManager.
|
|
18
|
+
Use read_np_buffer to send a captured frame to the LLM for reasoning.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
driver: CameraDriver | None = None,
|
|
24
|
+
scaling: int | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__()
|
|
27
|
+
self._driver: CameraDriver = driver or V4L2CameraDriver()
|
|
28
|
+
self._scaling: int | None = scaling
|
|
29
|
+
self._active_camera_id: int | None = None
|
|
30
|
+
|
|
31
|
+
def _resize_if_needed(self, frame: cv2.Mat) -> cv2.Mat:
|
|
32
|
+
"""Resize frame if scaling is configured."""
|
|
33
|
+
if self._scaling is None or self._scaling <= 0:
|
|
34
|
+
return frame
|
|
35
|
+
h, w = frame.shape[:2]
|
|
36
|
+
if w > h:
|
|
37
|
+
new_w = self._scaling
|
|
38
|
+
new_h = round(h * self._scaling / w)
|
|
39
|
+
else:
|
|
40
|
+
new_h = self._scaling
|
|
41
|
+
new_w = round(w * self._scaling / h)
|
|
42
|
+
return cv2.resize(frame, (new_w, new_h))
|
|
43
|
+
|
|
44
|
+
@tool(description="List all available cameras.")
|
|
45
|
+
def list_cameras(self) -> str:
|
|
46
|
+
cameras = self._driver.list_cameras()
|
|
47
|
+
if not cameras:
|
|
48
|
+
return "No cameras found."
|
|
49
|
+
return "Available cameras:\n" + "\n".join(
|
|
50
|
+
f" Camera ID: {cid}, Driver: {self._driver.driver_name}, Description: {name}"
|
|
51
|
+
for cid, name in cameras
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@tool(description="Open a camera by ID. Must be called before grab_image.")
|
|
55
|
+
def open_camera(self, camera_id: int) -> str:
|
|
56
|
+
if self._active_camera_id is not None:
|
|
57
|
+
return "ERROR: A camera is already open. Call close_camera first."
|
|
58
|
+
|
|
59
|
+
ok = self._driver.open(camera_id)
|
|
60
|
+
if not ok:
|
|
61
|
+
return f"ERROR: Camera {camera_id} could not be opened."
|
|
62
|
+
|
|
63
|
+
self._active_camera_id = camera_id
|
|
64
|
+
|
|
65
|
+
ok, frame, _ = self._driver.grab_frame()
|
|
66
|
+
if not ok or frame is None:
|
|
67
|
+
self._driver.close()
|
|
68
|
+
self._active_camera_id = None
|
|
69
|
+
return f"ERROR: Camera {camera_id} warm-up failed."
|
|
70
|
+
|
|
71
|
+
return f"OK: Camera {camera_id} (driver: {self._driver.driver_name}) opened."
|
|
72
|
+
|
|
73
|
+
@tool(description="Close the currently open camera.")
|
|
74
|
+
def close_camera(self) -> str:
|
|
75
|
+
if self._active_camera_id is None:
|
|
76
|
+
return "No camera is currently open."
|
|
77
|
+
|
|
78
|
+
self._driver.close()
|
|
79
|
+
self._active_camera_id = None
|
|
80
|
+
return "OK: Camera closed."
|
|
81
|
+
|
|
82
|
+
@tool(description="Capture a single image from the open camera and store it in the ImageBufferManager. Pass buffer_name to override the default name 'image:camera:<id>'.")
|
|
83
|
+
def grab_image(self, buffer_name: str | None = None) -> str:
|
|
84
|
+
if self._active_camera_id is None:
|
|
85
|
+
return "ERROR: No camera is open."
|
|
86
|
+
|
|
87
|
+
ok, frame, _ = self._driver.grab_frame()
|
|
88
|
+
if not ok or frame is None:
|
|
89
|
+
return "ERROR: Failed to capture image."
|
|
90
|
+
|
|
91
|
+
frame = self._resize_if_needed(frame)
|
|
92
|
+
|
|
93
|
+
name = buffer_name if buffer_name is not None else f"image:camera:{self._active_camera_id}"
|
|
94
|
+
self._store_np_buffer(name, frame)
|
|
95
|
+
|
|
96
|
+
return f"OK: Image captured ({frame.shape[1]}x{frame.shape[0]}), stored as '{name}'."
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
|
|
7
|
+
from peteos import AgenticObject, tool
|
|
8
|
+
|
|
9
|
+
from petekit.numpy_buffer_manager import NumPyBufferManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ImageRegion:
|
|
14
|
+
"""Pixel-based region of an image. Coordinates are 0-based and inclusive. (x1, y1) is the top-left corner, (x2, y2) is the bottom-right corner."""
|
|
15
|
+
x1: int
|
|
16
|
+
y1: int
|
|
17
|
+
x2: int
|
|
18
|
+
y2: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ImageBufferManager(NumPyBufferManager, AgenticObject):
|
|
22
|
+
"""
|
|
23
|
+
- For image-like numpy buffers, you can call read_np_buffer to have a look at them.
|
|
24
|
+
- The read_np_buffer tool will transform image coordinates into numpy coordinates for you.
|
|
25
|
+
- Image-like buffer names are usually prefixed with "image:"
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@tool(description="Read a NumPy buffer as an image and send it to the LLM for reasoning. Pass an ImageRegion for pixel-based region cropping (0-based, inclusive): (x1, y1) is the top-left corner, (x2, y2) is the bottom-right corner. Omit the region to read the full image.")
|
|
29
|
+
async def read_np_buffer(self, name: str, runner, region: ImageRegion | None = None) -> str:
|
|
30
|
+
"""Read a buffer (or region) and send it to the LLM. Returns a hint; the LLM sees the image directly."""
|
|
31
|
+
if name not in self._buffers:
|
|
32
|
+
return f"Error: no buffer named '{name}'. Use copy_np_buffer or an inheriting agent to create it."
|
|
33
|
+
buf = self._buffers[name]
|
|
34
|
+
array = buf.array
|
|
35
|
+
|
|
36
|
+
if region is None:
|
|
37
|
+
region_array = array
|
|
38
|
+
else:
|
|
39
|
+
x1, y1, x2, y2 = region.x1, region.y1, region.x2, region.y2
|
|
40
|
+
if x1 > x2 or y1 > y2:
|
|
41
|
+
return f"Error: region x1={x1} > x2={x2} or y1={y1} > y2={y2}. Buffer shape is {array.shape}."
|
|
42
|
+
if x2 >= array.shape[1] or y2 >= array.shape[0]:
|
|
43
|
+
return f"Error: x2={x2} or y2={y2} exceeds buffer shape {array.shape}."
|
|
44
|
+
region_array = array[y1:y2+1, x1:x2+1]
|
|
45
|
+
|
|
46
|
+
total_pixels = region_array.shape[0] * region_array.shape[1]
|
|
47
|
+
if total_pixels > NumPyBufferManager._MAX_PIXELS:
|
|
48
|
+
actual_w, actual_h = array.shape[1], array.shape[0]
|
|
49
|
+
return (
|
|
50
|
+
f"Requested region for buffer '{name}' has {total_pixels} pixels, exceeding the {NumPyBufferManager._MAX_PIXELS}-pixel limit "
|
|
51
|
+
f"(image is {actual_w}x{actual_h}={actual_w*actual_h} pixels). "
|
|
52
|
+
f"Use a smaller ImageRegion to stay under the limit."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
encode_ok, png_bytes = cv2.imencode(".png", region_array)
|
|
56
|
+
if not encode_ok:
|
|
57
|
+
return f"Error: cv2.imencode failed for buffer '{name}'."
|
|
58
|
+
image_data = png_bytes.tobytes()
|
|
59
|
+
|
|
60
|
+
await self._send_media(
|
|
61
|
+
data=image_data,
|
|
62
|
+
mime_type="image/png",
|
|
63
|
+
runner=runner,
|
|
64
|
+
)
|
|
65
|
+
return f"Buffer '{name}' ({region_array.shape}) sent to LLM for reasoning."
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""MultiCameraObserver - manages multiple camera drivers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import cv2
|
|
8
|
+
|
|
9
|
+
from peteos.agentic_objects.camera_driver import CameraDriver
|
|
10
|
+
from peteos.oap.agentic_object import AgenticObject
|
|
11
|
+
from peteos.oap.decorators import tool
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from peteos.session import Session
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MultiCameraObserver(AgenticObject):
|
|
18
|
+
"""Observer with access to multiple camera drivers.
|
|
19
|
+
|
|
20
|
+
Registers drivers by name. When listing cameras, returns
|
|
21
|
+
the driver name, camera ID, and description for all drivers.
|
|
22
|
+
Only one camera (from any driver) can be open at a time.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, scaling: int | None = None) -> None:
|
|
26
|
+
super().__init__()
|
|
27
|
+
self._drivers: dict[str, CameraDriver] = {}
|
|
28
|
+
self._scaling: int | None = scaling
|
|
29
|
+
self._active_driver: CameraDriver | None = None
|
|
30
|
+
self._active_camera_id: int | None = None
|
|
31
|
+
self._cached_frame: bytes | None = None
|
|
32
|
+
self._cached_shape: tuple[int, int] | None = None
|
|
33
|
+
|
|
34
|
+
def register_driver(self, driver: CameraDriver) -> None:
|
|
35
|
+
"""Register a camera driver by name."""
|
|
36
|
+
self._drivers[driver.driver_name] = driver
|
|
37
|
+
|
|
38
|
+
@tool(description="List all available cameras across all registered drivers.")
|
|
39
|
+
def list_cameras(self) -> str:
|
|
40
|
+
cameras: list[tuple[str, int, str]] = []
|
|
41
|
+
for driver in self._drivers.values():
|
|
42
|
+
for cid, desc in driver.list_cameras():
|
|
43
|
+
cameras.append((driver.driver_name, cid, desc))
|
|
44
|
+
if not cameras:
|
|
45
|
+
return "No cameras found."
|
|
46
|
+
lines = []
|
|
47
|
+
for driver_name, cid, desc in cameras:
|
|
48
|
+
lines.append(
|
|
49
|
+
f" Driver: {driver_name}, Camera ID: {cid}, Description: {desc}"
|
|
50
|
+
)
|
|
51
|
+
return "Available cameras:\n" + "\n".join(lines)
|
|
52
|
+
|
|
53
|
+
@tool(description="Open a camera from a specific driver.")
|
|
54
|
+
def open_camera(self, driver_name: str, camera_id: int) -> str:
|
|
55
|
+
if self._active_driver is not None:
|
|
56
|
+
return "ERROR: A camera is already open. Call close_camera first."
|
|
57
|
+
|
|
58
|
+
driver = self._drivers.get(driver_name)
|
|
59
|
+
if driver is None:
|
|
60
|
+
return f"ERROR: Unknown driver '{driver_name}'. Register it first."
|
|
61
|
+
|
|
62
|
+
if not driver.open(camera_id):
|
|
63
|
+
return f"ERROR: Camera {camera_id} on driver '{driver_name}' could not be opened."
|
|
64
|
+
|
|
65
|
+
self._active_driver = driver
|
|
66
|
+
self._active_camera_id = camera_id
|
|
67
|
+
self._cached_frame = None
|
|
68
|
+
self._cached_shape = None
|
|
69
|
+
|
|
70
|
+
# Warm up: grab a frame.
|
|
71
|
+
ok, frame, _ = driver.grab_frame()
|
|
72
|
+
if not ok or frame is None:
|
|
73
|
+
driver.close()
|
|
74
|
+
self._active_driver = None
|
|
75
|
+
self._active_camera_id = None
|
|
76
|
+
self._cached_frame = None
|
|
77
|
+
self._cached_shape = None
|
|
78
|
+
return f"ERROR: Camera warm-up failed on '{driver_name}'."
|
|
79
|
+
|
|
80
|
+
return f"OK: Camera {camera_id} opened (driver: {driver_name})."
|
|
81
|
+
|
|
82
|
+
@tool(description="Close the currently open camera.")
|
|
83
|
+
def close_camera(self) -> str:
|
|
84
|
+
if self._active_driver is None:
|
|
85
|
+
return "No camera is currently open."
|
|
86
|
+
|
|
87
|
+
self._active_driver.close()
|
|
88
|
+
self._active_driver = None
|
|
89
|
+
self._active_camera_id = None
|
|
90
|
+
self._cached_frame = None
|
|
91
|
+
self._cached_shape = None
|
|
92
|
+
return "OK: Camera closed."
|
|
93
|
+
|
|
94
|
+
@tool(description="Capture a single image from the open camera. Caches the frame in memory.")
|
|
95
|
+
def grab_image(self) -> str:
|
|
96
|
+
if self._active_driver is None:
|
|
97
|
+
return "ERROR: No camera is open."
|
|
98
|
+
|
|
99
|
+
ok, frame, (raw_w, raw_h) = self._active_driver.grab_frame()
|
|
100
|
+
if not ok or frame is None:
|
|
101
|
+
return "ERROR: Failed to capture image."
|
|
102
|
+
|
|
103
|
+
# Resize if scaling is configured.
|
|
104
|
+
if self._scaling is not None and self._scaling > 0:
|
|
105
|
+
h, w = frame.shape[:2]
|
|
106
|
+
if w > h:
|
|
107
|
+
new_w = self._scaling
|
|
108
|
+
new_h = round(h * self._scaling / w)
|
|
109
|
+
else:
|
|
110
|
+
new_h = self._scaling
|
|
111
|
+
new_w = round(w * self._scaling / h)
|
|
112
|
+
frame = cv2.resize(frame, (new_w, new_h))
|
|
113
|
+
|
|
114
|
+
# Encode as PNG.
|
|
115
|
+
_, encoded = cv2.imencode(".png", frame)
|
|
116
|
+
self._cached_frame = encoded.tobytes()
|
|
117
|
+
self._cached_shape = (frame.shape[1], frame.shape[0])
|
|
118
|
+
return f"OK: Image captured ({self._cached_shape[0]}x{self._cached_shape[1]}), cached in memory."
|
|
119
|
+
|
|
120
|
+
@tool(description="Read the cached image from the last grab_image call and send it to the session.")
|
|
121
|
+
async def read_cached_image(self, session: "Session | None" = None) -> str:
|
|
122
|
+
if self._cached_frame is None:
|
|
123
|
+
return "ERROR: No image has been captured yet. Call grab_image first."
|
|
124
|
+
|
|
125
|
+
if session is None:
|
|
126
|
+
return "ERROR: Session not available."
|
|
127
|
+
|
|
128
|
+
await self._send_media(
|
|
129
|
+
data=self._cached_frame,
|
|
130
|
+
mime_type="image/png",
|
|
131
|
+
session=session,
|
|
132
|
+
)
|
|
133
|
+
return "OK: Image sent to session."
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Screenshooter - capture the screen and store images in the ImageBufferManager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import mss
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from peteos import AgenticObject, tool
|
|
10
|
+
|
|
11
|
+
from .image_buffer_manager import ImageBufferManager, ImageRegion
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Screenshooter(ImageBufferManager, AgenticObject):
|
|
15
|
+
"""
|
|
16
|
+
- You can call the take_screenshoot tool to take a screenshoot and store it into a numby buffer.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@tool(description="")
|
|
20
|
+
def take_screenshot(self, name: str, region: ImageRegion | None = None) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Take a screenshot and store it into a numby buffer.
|
|
23
|
+
Pass region as a dict with keys x1, y1 (top-left corner) and x2, y2 (bottom-right corner) to capture only that pixel region.
|
|
24
|
+
Omit region entirely to capture the full screen.
|
|
25
|
+
When calling read_np_buffer, use the exact buffer name from the return message.
|
|
26
|
+
"""
|
|
27
|
+
with mss.MSS() as s:
|
|
28
|
+
if region is None:
|
|
29
|
+
monitor = s.monitors[0]
|
|
30
|
+
else:
|
|
31
|
+
monitor = (region.x1, region.y1, region.x2, region.y2)
|
|
32
|
+
screenshot = s.grab(monitor)
|
|
33
|
+
|
|
34
|
+
array_bgr = cv2.cvtColor(np.asarray(screenshot), cv2.COLOR_BGRA2BGR)
|
|
35
|
+
|
|
36
|
+
buffer_name = name if name.startswith("image:") else f"image:{name}"
|
|
37
|
+
self._store_np_buffer(buffer_name, array_bgr)
|
|
38
|
+
|
|
39
|
+
h, w = array_bgr.shape[:2]
|
|
40
|
+
return f"Screenshot stored in buffer '{buffer_name}' ({w}x{h} pixels). Use read_np_buffer to send it to the LLM."
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: petekit
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: PeteOS Kit — agentic building blocks for streams, text, and images
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: peteos
|
|
8
|
+
Requires-Dist: diff-match-patch
|
|
9
|
+
Provides-Extra: image
|
|
10
|
+
Requires-Dist: opencv-python>=4.8.0; extra == "image"
|
|
11
|
+
Requires-Dist: numpy>=1.24.0; extra == "image"
|
|
12
|
+
Requires-Dist: mss; extra == "image"
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
petekit/image/__init__.py
|
|
5
|
+
petekit/image/camera_observer.py
|
|
6
|
+
petekit/image/image_buffer_manager.py
|
|
7
|
+
petekit/image/multi_camera_observer.py
|
|
8
|
+
petekit/image/screenshooter.py
|
|
9
|
+
petekit/petekit.egg-info/PKG-INFO
|
|
10
|
+
petekit/petekit.egg-info/SOURCES.txt
|
|
11
|
+
petekit/petekit.egg-info/dependency_links.txt
|
|
12
|
+
petekit/petekit.egg-info/requires.txt
|
|
13
|
+
petekit/petekit.egg-info/top_level.txt
|
|
14
|
+
petekit/stream/__init__.py
|
|
15
|
+
petekit/stream/basher.py
|
|
16
|
+
petekit/stream/connector.py
|
|
17
|
+
petekit/stream/sandboxed_basher.py
|
|
18
|
+
petekit/stream/stream_buffer_manager.py
|
|
19
|
+
petekit/stream/stream_forwarder.py
|
|
20
|
+
petekit/stream/stream_observer.py
|
|
21
|
+
petekit/stream/pattern_matchers/fsa_stream.py
|
|
22
|
+
petekit/stream/pattern_matchers/grammar_stream.py
|
|
23
|
+
petekit/stream/pattern_matchers/heavy_hitter_stream.py
|
|
24
|
+
petekit/stream/pattern_matchers/multiline_regex_stream.py
|
|
25
|
+
petekit/stream/pattern_matchers/regex_stream.py
|
|
26
|
+
petekit/text/__init__.py
|
|
27
|
+
petekit/text/buffer_manager.py
|
|
28
|
+
petekit/text/text_editor.py
|
|
29
|
+
petekit/text/web_navigator.py
|
|
30
|
+
petekit/tools/__init__.py
|
|
31
|
+
petekit/tools/psh.py
|
|
32
|
+
petekit/utils/__init__.py
|
|
33
|
+
petekit/utils/three_merge.py
|
|
34
|
+
petekit/utils/camera_driver/__init__.py
|
|
35
|
+
petekit/utils/camera_driver/camera_driver.py
|
|
36
|
+
petekit/utils/camera_driver/cv2_camera_driver.py
|
|
37
|
+
petekit/utils/camera_driver/v4l2_camera_driver.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from petekit.stream.stream_buffer_manager import StreamBufferManager, StreamBufferRules, Rule
|
|
2
|
+
from petekit.stream.stream_observer import StreamObserver
|
|
3
|
+
from petekit.stream.stream_forwarder import StreamForwarder
|
|
4
|
+
from petekit.stream.pattern_matchers.regex_stream import RegexStreamObserver, RegexPattern
|
|
5
|
+
from petekit.stream.basher import Basher, BashHandle
|
|
6
|
+
from petekit.stream.connector import Connector, ConnectionHandle
|
|
7
|
+
from petekit.stream.sandboxed_basher import SandboxedBasher
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"StreamBufferManager",
|
|
11
|
+
"StreamBufferRules",
|
|
12
|
+
"Rule",
|
|
13
|
+
"StreamObserver",
|
|
14
|
+
"StreamForwarder",
|
|
15
|
+
"RegexStreamObserver",
|
|
16
|
+
"RegexPattern",
|
|
17
|
+
"Basher",
|
|
18
|
+
"BashHandle",
|
|
19
|
+
"Connector",
|
|
20
|
+
"ConnectionHandle",
|
|
21
|
+
"SandboxedBasher",
|
|
22
|
+
]
|