foodforthought-cli 0.2.7__py3-none-any.whl → 0.3.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.
- ate/__init__.py +6 -0
- ate/__main__.py +16 -0
- ate/auth/__init__.py +1 -0
- ate/auth/device_flow.py +141 -0
- ate/auth/token_store.py +96 -0
- ate/behaviors/__init__.py +100 -0
- ate/behaviors/approach.py +399 -0
- ate/behaviors/common.py +686 -0
- ate/behaviors/tree.py +454 -0
- ate/cli.py +855 -3995
- ate/client.py +90 -0
- ate/commands/__init__.py +168 -0
- ate/commands/auth.py +389 -0
- ate/commands/bridge.py +448 -0
- ate/commands/data.py +185 -0
- ate/commands/deps.py +111 -0
- ate/commands/generate.py +384 -0
- ate/commands/memory.py +907 -0
- ate/commands/parts.py +166 -0
- ate/commands/primitive.py +399 -0
- ate/commands/protocol.py +288 -0
- ate/commands/recording.py +524 -0
- ate/commands/repo.py +154 -0
- ate/commands/simulation.py +291 -0
- ate/commands/skill.py +303 -0
- ate/commands/skills.py +487 -0
- ate/commands/team.py +147 -0
- ate/commands/workflow.py +271 -0
- ate/detection/__init__.py +38 -0
- ate/detection/base.py +142 -0
- ate/detection/color_detector.py +399 -0
- ate/detection/trash_detector.py +322 -0
- ate/drivers/__init__.py +39 -0
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +942 -0
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +187 -0
- ate/interfaces/base.py +273 -0
- ate/interfaces/body.py +267 -0
- ate/interfaces/detection.py +282 -0
- ate/interfaces/locomotion.py +422 -0
- ate/interfaces/manipulation.py +408 -0
- ate/interfaces/navigation.py +389 -0
- ate/interfaces/perception.py +362 -0
- ate/interfaces/sensors.py +247 -0
- ate/interfaces/types.py +371 -0
- ate/llm_proxy.py +239 -0
- ate/mcp_server.py +387 -0
- ate/memory/__init__.py +35 -0
- ate/memory/cloud.py +244 -0
- ate/memory/context.py +269 -0
- ate/memory/embeddings.py +184 -0
- ate/memory/export.py +26 -0
- ate/memory/merge.py +146 -0
- ate/memory/migrate/__init__.py +34 -0
- ate/memory/migrate/base.py +89 -0
- ate/memory/migrate/pipeline.py +189 -0
- ate/memory/migrate/sources/__init__.py +13 -0
- ate/memory/migrate/sources/chroma.py +170 -0
- ate/memory/migrate/sources/pinecone.py +120 -0
- ate/memory/migrate/sources/qdrant.py +110 -0
- ate/memory/migrate/sources/weaviate.py +160 -0
- ate/memory/reranker.py +353 -0
- ate/memory/search.py +26 -0
- ate/memory/store.py +548 -0
- ate/recording/__init__.py +83 -0
- ate/recording/demonstration.py +378 -0
- ate/recording/session.py +415 -0
- ate/recording/upload.py +304 -0
- ate/recording/visual.py +416 -0
- ate/recording/wrapper.py +95 -0
- ate/robot/__init__.py +221 -0
- ate/robot/agentic_servo.py +856 -0
- ate/robot/behaviors.py +493 -0
- ate/robot/ble_capture.py +1000 -0
- ate/robot/ble_enumerate.py +506 -0
- ate/robot/calibration.py +668 -0
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +3735 -0
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +441 -0
- ate/robot/introspection.py +330 -0
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/manager.py +270 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +281 -0
- ate/robot/registry.py +322 -0
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +675 -0
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +1048 -0
- ate/robot/visual_servo_loop.py +494 -0
- ate/robot/visual_servoing.py +570 -0
- ate/robot/visual_system_id.py +906 -0
- ate/transports/__init__.py +121 -0
- ate/transports/base.py +394 -0
- ate/transports/ble.py +405 -0
- ate/transports/hybrid.py +444 -0
- ate/transports/serial.py +345 -0
- ate/urdf/__init__.py +30 -0
- ate/urdf/capture.py +582 -0
- ate/urdf/cloud.py +491 -0
- ate/urdf/collision.py +271 -0
- ate/urdf/commands.py +708 -0
- ate/urdf/depth.py +360 -0
- ate/urdf/inertial.py +312 -0
- ate/urdf/kinematics.py +330 -0
- ate/urdf/lifting.py +415 -0
- ate/urdf/meshing.py +300 -0
- ate/urdf/models/__init__.py +110 -0
- ate/urdf/models/depth_anything.py +253 -0
- ate/urdf/models/sam2.py +324 -0
- ate/urdf/motion_analysis.py +396 -0
- ate/urdf/pipeline.py +468 -0
- ate/urdf/scale.py +256 -0
- ate/urdf/scan_session.py +411 -0
- ate/urdf/segmentation.py +299 -0
- ate/urdf/synthesis.py +319 -0
- ate/urdf/topology.py +336 -0
- ate/urdf/validation.py +371 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
ate/__init__.py
CHANGED
ate/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enable running ate as a module: python -m ate
|
|
4
|
+
|
|
5
|
+
This allows the CLI to be invoked via:
|
|
6
|
+
python -m ate --help
|
|
7
|
+
python -m ate robot upload mechdog --dry-run
|
|
8
|
+
python -m ate login
|
|
9
|
+
|
|
10
|
+
Instead of requiring the installed entry point.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ate.cli import main
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
ate/auth/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""OAuth 2.0 Device Authorization Grant (RFC 8628) auth package."""
|
ate/auth/device_flow.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth 2.0 Device Authorization Grant (RFC 8628) client.
|
|
3
|
+
|
|
4
|
+
Provides agent-friendly authentication for the `ate` CLI.
|
|
5
|
+
The device flow allows headless/CLI clients to authenticate
|
|
6
|
+
by having the user authorize on a separate device with a browser.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DeviceFlowError(Exception):
|
|
16
|
+
"""General device flow error."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DeviceFlowTimeout(DeviceFlowError):
|
|
21
|
+
"""Polling timed out waiting for user authorization."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DeviceFlowDenied(DeviceFlowError):
|
|
26
|
+
"""User denied the authorization request."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DeviceCodeResponse:
|
|
32
|
+
"""Response from the device authorization endpoint."""
|
|
33
|
+
device_code: str
|
|
34
|
+
user_code: str # e.g. "ABCD-1234" — human types this
|
|
35
|
+
verification_uri: str # e.g. "https://kindly.fyi/device"
|
|
36
|
+
expires_in: int # seconds (default 600)
|
|
37
|
+
interval: int # polling interval seconds (default 5)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class TokenResponse:
|
|
42
|
+
"""Token response from the token endpoint."""
|
|
43
|
+
access_token: str
|
|
44
|
+
refresh_token: str
|
|
45
|
+
expires_in: int # seconds
|
|
46
|
+
token_type: str # "Bearer"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DeviceFlowClient:
|
|
50
|
+
"""OAuth 2.0 Device Authorization Grant (RFC 8628) client."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, server_url: str = "https://kindly.fyi"):
|
|
53
|
+
self.server_url = server_url
|
|
54
|
+
|
|
55
|
+
def request_code(self) -> DeviceCodeResponse:
|
|
56
|
+
"""POST /api/auth/device/code → DeviceCodeResponse.
|
|
57
|
+
|
|
58
|
+
Raises DeviceFlowError on HTTP or network errors.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
resp = requests.post(
|
|
62
|
+
f"{self.server_url}/api/auth/device/code",
|
|
63
|
+
json={"client_id": "ate-cli"},
|
|
64
|
+
timeout=10,
|
|
65
|
+
)
|
|
66
|
+
resp.raise_for_status()
|
|
67
|
+
data = resp.json()
|
|
68
|
+
return DeviceCodeResponse(
|
|
69
|
+
device_code=data["device_code"],
|
|
70
|
+
user_code=data["user_code"],
|
|
71
|
+
verification_uri=data["verification_uri"],
|
|
72
|
+
expires_in=data["expires_in"],
|
|
73
|
+
interval=data["interval"],
|
|
74
|
+
)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
if isinstance(e, DeviceFlowError):
|
|
77
|
+
raise
|
|
78
|
+
raise DeviceFlowError(f"Failed to request device code: {e}") from e
|
|
79
|
+
|
|
80
|
+
def poll_for_token(
|
|
81
|
+
self,
|
|
82
|
+
device_code: str,
|
|
83
|
+
interval: int = 5,
|
|
84
|
+
expires_in: int = 600,
|
|
85
|
+
) -> TokenResponse:
|
|
86
|
+
"""Poll POST /api/auth/device/token until authorized or expired.
|
|
87
|
+
|
|
88
|
+
Returns TokenResponse on success.
|
|
89
|
+
Raises DeviceFlowTimeout if expires_in exceeded.
|
|
90
|
+
Raises DeviceFlowDenied if user denied.
|
|
91
|
+
"""
|
|
92
|
+
current_interval = interval
|
|
93
|
+
start_time = time.time()
|
|
94
|
+
|
|
95
|
+
while True:
|
|
96
|
+
time.sleep(current_interval)
|
|
97
|
+
|
|
98
|
+
elapsed = time.time() - start_time
|
|
99
|
+
if elapsed > expires_in:
|
|
100
|
+
raise DeviceFlowTimeout(
|
|
101
|
+
f"Device authorization timed out after {expires_in}s"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
resp = requests.post(
|
|
105
|
+
f"{self.server_url}/api/auth/device/token",
|
|
106
|
+
json={
|
|
107
|
+
"client_id": "ate-cli",
|
|
108
|
+
"device_code": device_code,
|
|
109
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
110
|
+
},
|
|
111
|
+
timeout=10,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if resp.status_code == 200:
|
|
115
|
+
data = resp.json()
|
|
116
|
+
return TokenResponse(
|
|
117
|
+
access_token=data["access_token"],
|
|
118
|
+
refresh_token=data["refresh_token"],
|
|
119
|
+
expires_in=data["expires_in"],
|
|
120
|
+
token_type=data["token_type"],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Handle error responses (400-level)
|
|
124
|
+
try:
|
|
125
|
+
error_data = resp.json()
|
|
126
|
+
except Exception:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
error = error_data.get("error", "")
|
|
130
|
+
|
|
131
|
+
if error == "authorization_pending":
|
|
132
|
+
continue
|
|
133
|
+
elif error == "slow_down":
|
|
134
|
+
current_interval += 5
|
|
135
|
+
continue
|
|
136
|
+
elif error == "access_denied":
|
|
137
|
+
raise DeviceFlowDenied("User denied the authorization request")
|
|
138
|
+
elif error == "expired_token":
|
|
139
|
+
raise DeviceFlowTimeout("Device code expired")
|
|
140
|
+
else:
|
|
141
|
+
continue
|
ate/auth/token_store.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Token persistence for OAuth 2.0 device flow credentials.
|
|
3
|
+
|
|
4
|
+
Stores tokens at ~/.ate/credentials.json with expiry tracking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from ate.auth.device_flow import TokenResponse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TokenStore:
|
|
16
|
+
"""Manages token persistence at ~/.ate/credentials.json."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, path: str = None):
|
|
19
|
+
self.path = path or os.path.expanduser("~/.ate/credentials.json")
|
|
20
|
+
|
|
21
|
+
def save(self, token_response: TokenResponse) -> None:
|
|
22
|
+
"""Save tokens to disk (creates parent dirs).
|
|
23
|
+
|
|
24
|
+
Records saved_at timestamp for expiry calculations.
|
|
25
|
+
"""
|
|
26
|
+
parent = os.path.dirname(self.path)
|
|
27
|
+
if parent:
|
|
28
|
+
os.makedirs(parent, exist_ok=True)
|
|
29
|
+
|
|
30
|
+
data = {
|
|
31
|
+
"access_token": token_response.access_token,
|
|
32
|
+
"refresh_token": token_response.refresh_token,
|
|
33
|
+
"expires_in": token_response.expires_in,
|
|
34
|
+
"token_type": token_response.token_type,
|
|
35
|
+
"saved_at": time.time(),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
with open(self.path, "w") as f:
|
|
39
|
+
json.dump(data, f, indent=2)
|
|
40
|
+
|
|
41
|
+
def load(self) -> Optional[TokenResponse]:
|
|
42
|
+
"""Load tokens from disk. Returns None if no credentials or invalid."""
|
|
43
|
+
if not os.path.exists(self.path):
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
with open(self.path) as f:
|
|
48
|
+
data = json.load(f)
|
|
49
|
+
return TokenResponse(
|
|
50
|
+
access_token=data["access_token"],
|
|
51
|
+
refresh_token=data["refresh_token"],
|
|
52
|
+
expires_in=data["expires_in"],
|
|
53
|
+
token_type=data["token_type"],
|
|
54
|
+
)
|
|
55
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def clear(self) -> None:
|
|
59
|
+
"""Delete stored credentials."""
|
|
60
|
+
try:
|
|
61
|
+
os.remove(self.path)
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
def _load_raw(self) -> Optional[dict]:
|
|
66
|
+
"""Load raw JSON data including saved_at."""
|
|
67
|
+
if not os.path.exists(self.path):
|
|
68
|
+
return None
|
|
69
|
+
try:
|
|
70
|
+
with open(self.path) as f:
|
|
71
|
+
return json.load(f)
|
|
72
|
+
except (json.JSONDecodeError, TypeError):
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def _expires_at(self) -> Optional[float]:
|
|
76
|
+
"""Calculate the absolute expiry time."""
|
|
77
|
+
data = self._load_raw()
|
|
78
|
+
if data is None:
|
|
79
|
+
return None
|
|
80
|
+
saved_at = data.get("saved_at", 0)
|
|
81
|
+
expires_in = data.get("expires_in", 0)
|
|
82
|
+
return saved_at + expires_in
|
|
83
|
+
|
|
84
|
+
def is_expired(self) -> bool:
|
|
85
|
+
"""Check if the access token has expired."""
|
|
86
|
+
expires_at = self._expires_at()
|
|
87
|
+
if expires_at is None:
|
|
88
|
+
return True
|
|
89
|
+
return time.time() >= expires_at
|
|
90
|
+
|
|
91
|
+
def needs_refresh(self) -> bool:
|
|
92
|
+
"""Check if token is within 5 minutes of expiry."""
|
|
93
|
+
expires_at = self._expires_at()
|
|
94
|
+
if expires_at is None:
|
|
95
|
+
return True
|
|
96
|
+
return time.time() >= (expires_at - 300)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Behavior Tree framework for composing robot skills.
|
|
3
|
+
|
|
4
|
+
Behavior trees allow complex behaviors to be built from simple skills
|
|
5
|
+
using a tree structure of:
|
|
6
|
+
- Sequences (do A, then B, then C)
|
|
7
|
+
- Selectors (try A, if fails try B)
|
|
8
|
+
- Conditions (if X is true...)
|
|
9
|
+
- Actions (do X)
|
|
10
|
+
|
|
11
|
+
This is how we turn small demos into valuable composite behaviors.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .tree import (
|
|
15
|
+
BehaviorNode,
|
|
16
|
+
BehaviorStatus,
|
|
17
|
+
Sequence,
|
|
18
|
+
Selector,
|
|
19
|
+
Parallel,
|
|
20
|
+
Action,
|
|
21
|
+
Condition,
|
|
22
|
+
Inverter,
|
|
23
|
+
Succeeder,
|
|
24
|
+
Repeater,
|
|
25
|
+
RepeatUntilFail,
|
|
26
|
+
BehaviorTree,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .common import (
|
|
30
|
+
# Navigation
|
|
31
|
+
NavigateToPoint,
|
|
32
|
+
NavigateToPose,
|
|
33
|
+
Patrol,
|
|
34
|
+
ReturnHome,
|
|
35
|
+
# Detection
|
|
36
|
+
DetectObject,
|
|
37
|
+
IsObjectVisible,
|
|
38
|
+
FindNearest,
|
|
39
|
+
ApproachObject,
|
|
40
|
+
# Manipulation
|
|
41
|
+
PickUp,
|
|
42
|
+
PlaceAt,
|
|
43
|
+
DropInBin,
|
|
44
|
+
# Conditions
|
|
45
|
+
IsBatteryLow,
|
|
46
|
+
IsPathClear,
|
|
47
|
+
HasObject,
|
|
48
|
+
# Composite behaviors
|
|
49
|
+
PatrolAndCleanup,
|
|
50
|
+
SearchAndRetrieve,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
from .approach import (
|
|
54
|
+
ApproachState,
|
|
55
|
+
ApproachConfig,
|
|
56
|
+
ApproachTarget,
|
|
57
|
+
VisualServoApproach,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
# Core tree nodes
|
|
62
|
+
"BehaviorNode",
|
|
63
|
+
"BehaviorStatus",
|
|
64
|
+
"Sequence",
|
|
65
|
+
"Selector",
|
|
66
|
+
"Parallel",
|
|
67
|
+
"Action",
|
|
68
|
+
"Condition",
|
|
69
|
+
"Inverter",
|
|
70
|
+
"Succeeder",
|
|
71
|
+
"Repeater",
|
|
72
|
+
"RepeatUntilFail",
|
|
73
|
+
"BehaviorTree",
|
|
74
|
+
# Navigation actions
|
|
75
|
+
"NavigateToPoint",
|
|
76
|
+
"NavigateToPose",
|
|
77
|
+
"Patrol",
|
|
78
|
+
"ReturnHome",
|
|
79
|
+
# Detection actions
|
|
80
|
+
"DetectObject",
|
|
81
|
+
"IsObjectVisible",
|
|
82
|
+
"FindNearest",
|
|
83
|
+
"ApproachObject",
|
|
84
|
+
# Manipulation actions
|
|
85
|
+
"PickUp",
|
|
86
|
+
"PlaceAt",
|
|
87
|
+
"DropInBin",
|
|
88
|
+
# Conditions
|
|
89
|
+
"IsBatteryLow",
|
|
90
|
+
"IsPathClear",
|
|
91
|
+
"HasObject",
|
|
92
|
+
# Composite behaviors
|
|
93
|
+
"PatrolAndCleanup",
|
|
94
|
+
"SearchAndRetrieve",
|
|
95
|
+
# Approach behaviors (locomotion-agnostic)
|
|
96
|
+
"ApproachState",
|
|
97
|
+
"ApproachConfig",
|
|
98
|
+
"ApproachTarget",
|
|
99
|
+
"VisualServoApproach",
|
|
100
|
+
]
|