foodforthought-cli 0.2.8__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.
Files changed (116) hide show
  1. ate/__init__.py +6 -0
  2. ate/__main__.py +16 -0
  3. ate/auth/__init__.py +1 -0
  4. ate/auth/device_flow.py +141 -0
  5. ate/auth/token_store.py +96 -0
  6. ate/behaviors/__init__.py +12 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/cli.py +855 -4551
  9. ate/client.py +90 -0
  10. ate/commands/__init__.py +168 -0
  11. ate/commands/auth.py +389 -0
  12. ate/commands/bridge.py +448 -0
  13. ate/commands/data.py +185 -0
  14. ate/commands/deps.py +111 -0
  15. ate/commands/generate.py +384 -0
  16. ate/commands/memory.py +907 -0
  17. ate/commands/parts.py +166 -0
  18. ate/commands/primitive.py +399 -0
  19. ate/commands/protocol.py +288 -0
  20. ate/commands/recording.py +524 -0
  21. ate/commands/repo.py +154 -0
  22. ate/commands/simulation.py +291 -0
  23. ate/commands/skill.py +303 -0
  24. ate/commands/skills.py +487 -0
  25. ate/commands/team.py +147 -0
  26. ate/commands/workflow.py +271 -0
  27. ate/detection/__init__.py +38 -0
  28. ate/detection/base.py +142 -0
  29. ate/detection/color_detector.py +399 -0
  30. ate/detection/trash_detector.py +322 -0
  31. ate/drivers/__init__.py +18 -6
  32. ate/drivers/ble_transport.py +405 -0
  33. ate/drivers/mechdog.py +360 -24
  34. ate/drivers/wifi_camera.py +477 -0
  35. ate/interfaces/__init__.py +16 -0
  36. ate/interfaces/base.py +2 -0
  37. ate/interfaces/sensors.py +247 -0
  38. ate/llm_proxy.py +239 -0
  39. ate/memory/__init__.py +35 -0
  40. ate/memory/cloud.py +244 -0
  41. ate/memory/context.py +269 -0
  42. ate/memory/embeddings.py +184 -0
  43. ate/memory/export.py +26 -0
  44. ate/memory/merge.py +146 -0
  45. ate/memory/migrate/__init__.py +34 -0
  46. ate/memory/migrate/base.py +89 -0
  47. ate/memory/migrate/pipeline.py +189 -0
  48. ate/memory/migrate/sources/__init__.py +13 -0
  49. ate/memory/migrate/sources/chroma.py +170 -0
  50. ate/memory/migrate/sources/pinecone.py +120 -0
  51. ate/memory/migrate/sources/qdrant.py +110 -0
  52. ate/memory/migrate/sources/weaviate.py +160 -0
  53. ate/memory/reranker.py +353 -0
  54. ate/memory/search.py +26 -0
  55. ate/memory/store.py +548 -0
  56. ate/recording/__init__.py +42 -3
  57. ate/recording/session.py +12 -2
  58. ate/recording/visual.py +416 -0
  59. ate/robot/__init__.py +142 -0
  60. ate/robot/agentic_servo.py +856 -0
  61. ate/robot/behaviors.py +493 -0
  62. ate/robot/ble_capture.py +1000 -0
  63. ate/robot/ble_enumerate.py +506 -0
  64. ate/robot/calibration.py +88 -3
  65. ate/robot/calibration_state.py +388 -0
  66. ate/robot/commands.py +143 -11
  67. ate/robot/direction_calibration.py +554 -0
  68. ate/robot/discovery.py +104 -2
  69. ate/robot/llm_system_id.py +654 -0
  70. ate/robot/locomotion_calibration.py +508 -0
  71. ate/robot/marker_generator.py +611 -0
  72. ate/robot/perception.py +502 -0
  73. ate/robot/primitives.py +614 -0
  74. ate/robot/profiles.py +6 -0
  75. ate/robot/registry.py +5 -2
  76. ate/robot/servo_mapper.py +1153 -0
  77. ate/robot/skill_upload.py +285 -3
  78. ate/robot/target_calibration.py +500 -0
  79. ate/robot/teach.py +515 -0
  80. ate/robot/types.py +242 -0
  81. ate/robot/visual_labeler.py +9 -0
  82. ate/robot/visual_servo_loop.py +494 -0
  83. ate/robot/visual_servoing.py +570 -0
  84. ate/robot/visual_system_id.py +906 -0
  85. ate/transports/__init__.py +121 -0
  86. ate/transports/base.py +394 -0
  87. ate/transports/ble.py +405 -0
  88. ate/transports/hybrid.py +444 -0
  89. ate/transports/serial.py +345 -0
  90. ate/urdf/__init__.py +30 -0
  91. ate/urdf/capture.py +582 -0
  92. ate/urdf/cloud.py +491 -0
  93. ate/urdf/collision.py +271 -0
  94. ate/urdf/commands.py +708 -0
  95. ate/urdf/depth.py +360 -0
  96. ate/urdf/inertial.py +312 -0
  97. ate/urdf/kinematics.py +330 -0
  98. ate/urdf/lifting.py +415 -0
  99. ate/urdf/meshing.py +300 -0
  100. ate/urdf/models/__init__.py +110 -0
  101. ate/urdf/models/depth_anything.py +253 -0
  102. ate/urdf/models/sam2.py +324 -0
  103. ate/urdf/motion_analysis.py +396 -0
  104. ate/urdf/pipeline.py +468 -0
  105. ate/urdf/scale.py +256 -0
  106. ate/urdf/scan_session.py +411 -0
  107. ate/urdf/segmentation.py +299 -0
  108. ate/urdf/synthesis.py +319 -0
  109. ate/urdf/topology.py +336 -0
  110. ate/urdf/validation.py +371 -0
  111. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +1 -1
  112. foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
  113. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
  114. foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
  115. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
  116. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
ate/robot/skill_upload.py CHANGED
@@ -25,6 +25,116 @@ BASE_URL = os.getenv("ATE_API_URL", "https://www.kindly.fyi/api")
25
25
  CONFIG_FILE = Path.home() / ".ate" / "config.json"
26
26
 
27
27
 
28
+ class APIError(Exception):
29
+ """
30
+ Custom exception for API errors with helpful context.
31
+
32
+ Provides:
33
+ - Status code and reason
34
+ - Parsed error message from API response
35
+ - Suggestions for fixing common issues
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ message: str,
41
+ status_code: int = 0,
42
+ response_body: Optional[Dict] = None,
43
+ suggestions: Optional[List[str]] = None,
44
+ ):
45
+ self.status_code = status_code
46
+ self.response_body = response_body or {}
47
+ self.suggestions = suggestions or []
48
+
49
+ # Build helpful error message
50
+ parts = [message]
51
+
52
+ if self.response_body:
53
+ # Try to extract error details from response
54
+ error_detail = (
55
+ self.response_body.get("error") or
56
+ self.response_body.get("message") or
57
+ self.response_body.get("detail") or
58
+ self.response_body.get("errors")
59
+ )
60
+ if error_detail:
61
+ if isinstance(error_detail, list):
62
+ error_detail = "; ".join(str(e) for e in error_detail)
63
+ parts.append(f"Details: {error_detail}")
64
+
65
+ if self.suggestions:
66
+ parts.append("Suggestions:")
67
+ for s in self.suggestions:
68
+ parts.append(f" - {s}")
69
+
70
+ super().__init__("\n".join(parts))
71
+
72
+
73
+ def parse_api_error(response: requests.Response, endpoint: str) -> APIError:
74
+ """
75
+ Parse API error response and return helpful APIError.
76
+
77
+ Analyzes common error patterns and provides actionable suggestions.
78
+ """
79
+ status = response.status_code
80
+ suggestions = []
81
+
82
+ # Try to parse response body
83
+ try:
84
+ body = response.json()
85
+ except Exception:
86
+ body = {"raw": response.text[:500] if response.text else "No response body"}
87
+
88
+ # Common error patterns and suggestions
89
+ if status == 400:
90
+ error_text = str(body).lower()
91
+
92
+ if "project" in endpoint:
93
+ suggestions.append("Use --project-id to specify an existing project")
94
+ suggestions.append("Check project name doesn't contain special characters")
95
+ elif "calibration" in endpoint:
96
+ suggestions.append("Ensure calibration file contains required fields: name, version, method")
97
+ suggestions.append("Check robot slug exists: ate robot identify --search <robot-name>")
98
+
99
+ elif status == 401:
100
+ suggestions.append("Session may have expired. Run: ate login")
101
+ suggestions.append("Check ATE_API_KEY environment variable if using API key auth")
102
+
103
+ elif status == 403:
104
+ suggestions.append("You may not have permission for this operation")
105
+ suggestions.append("Check if the project/resource is owned by another user")
106
+
107
+ elif status == 404:
108
+ error_text = str(body).lower()
109
+
110
+ if "robot" in error_text and "not found" in error_text:
111
+ suggestions.append("Robot slug may not exist in the database")
112
+ suggestions.append("Search for robots: ate robot identify --search <partial-name>")
113
+ suggestions.append("List available robots: ate robot list")
114
+
115
+ elif "project" in endpoint:
116
+ suggestions.append("Project ID may be invalid or deleted")
117
+ suggestions.append("List your projects to find valid IDs")
118
+
119
+ elif status == 422:
120
+ suggestions.append("Request data validation failed")
121
+ if body.get("errors"):
122
+ for field, errors in body.get("errors", {}).items():
123
+ suggestions.append(f"Field '{field}': {errors}")
124
+
125
+ elif status >= 500:
126
+ suggestions.append("Server error - this is likely temporary")
127
+ suggestions.append("Try again in a few minutes")
128
+ suggestions.append("Report persistent issues at: https://github.com/kindlyrobotics/monorepo/issues")
129
+
130
+ return APIError(
131
+ message=f"API request failed: {status} {response.reason} for {endpoint}",
132
+ status_code=status,
133
+ response_body=body,
134
+ suggestions=suggestions,
135
+ )
136
+
137
+
28
138
  class SkillLibraryUploader:
29
139
  """
30
140
  Uploads skill libraries to FoodforThought.
@@ -68,11 +178,21 @@ class SkillLibraryUploader:
68
178
  )
69
179
 
70
180
  def _request(self, method: str, endpoint: str, **kwargs) -> Dict:
71
- """Make HTTP request to API."""
181
+ """
182
+ Make HTTP request to API with improved error handling.
183
+
184
+ Raises APIError with helpful suggestions on failure.
185
+ """
72
186
  url = f"{self.base_url}{endpoint}"
73
187
  response = requests.request(method, url, headers=self.headers, **kwargs)
74
- response.raise_for_status()
75
- return response.json()
188
+
189
+ if not response.ok:
190
+ raise parse_api_error(response, endpoint)
191
+
192
+ try:
193
+ return response.json()
194
+ except json.JSONDecodeError:
195
+ return {"raw": response.text}
76
196
 
77
197
  def get_or_create_project(self, name: str, description: str = "") -> str:
78
198
  """Get existing project or create a new one."""
@@ -334,6 +454,48 @@ class SkillLibraryUploader:
334
454
  labeler.calibrator.calibration = calibration
335
455
  skill_code = labeler.generate_skill_code(action)
336
456
 
457
+ # Determine skill type based on action structure
458
+ skill_type = "primitive" # Default
459
+ dependencies = []
460
+ hardware_requirements = []
461
+
462
+ # Infer hardware requirements from servo usage
463
+ servo_ids = set()
464
+ for step in action.steps:
465
+ if hasattr(step, 'pose_name') and step.pose_name:
466
+ pose = calibration.poses.get(step.pose_name)
467
+ if pose:
468
+ servo_ids.update(pose.servo_positions.keys())
469
+
470
+ # Map servo IDs to hardware requirements
471
+ for sid in servo_ids:
472
+ servo = calibration.servos.get(sid)
473
+ if servo:
474
+ if "gripper" in servo.name.lower():
475
+ if "gripper" not in hardware_requirements:
476
+ hardware_requirements.append("gripper")
477
+ elif "arm" in servo.name.lower() or "shoulder" in servo.name.lower() or "elbow" in servo.name.lower():
478
+ if "arm" not in hardware_requirements:
479
+ hardware_requirements.append("arm")
480
+ elif "leg" in servo.name.lower() or "hip" in servo.name.lower() or "thigh" in servo.name.lower():
481
+ if "legs" not in hardware_requirements:
482
+ hardware_requirements.append("legs")
483
+
484
+ # Determine skill type based on complexity
485
+ if len(action.steps) == 1:
486
+ skill_type = "primitive"
487
+ elif len(action.steps) <= 5:
488
+ skill_type = "compound"
489
+ else:
490
+ skill_type = "sequence"
491
+
492
+ # If action has perception tags, it's a behavior
493
+ if action.tags:
494
+ if any(t in ["perception", "detection", "visual", "feedback"] for t in action.tags):
495
+ skill_type = "behavior"
496
+ if "camera" not in hardware_requirements:
497
+ hardware_requirements.append("camera")
498
+
337
499
  response = self._request("POST", "/artifacts", json={
338
500
  "projectId": project_id,
339
501
  "name": action_name,
@@ -351,11 +513,131 @@ class SkillLibraryUploader:
351
513
  "tags": action.tags,
352
514
  "source": "visual_labeler",
353
515
  "generated_at": datetime.now().isoformat(),
516
+ # New skill hierarchy fields
517
+ "skill_type": skill_type, # "primitive", "compound", "behavior"
518
+ "dependencies": dependencies, # List of skill names this depends on
519
+ "hardware_requirements": hardware_requirements, # ["arm", "gripper", "legs", "camera"]
520
+ "servo_count": len(servo_ids),
354
521
  },
355
522
  })
356
523
  return response.get("artifact", {}).get("id")
357
524
 
358
525
 
526
+ def upload_primitives(
527
+ robot_name: str,
528
+ project_id: Optional[str] = None,
529
+ api_key: Optional[str] = None,
530
+ ) -> Dict[str, Any]:
531
+ """
532
+ Upload programmatic primitives from PrimitiveLibrary.
533
+
534
+ These are the cleanly-generated skills from primitives.py with proper
535
+ skill_type, dependencies, and hardware_requirements.
536
+
537
+ Args:
538
+ robot_name: Name of the robot
539
+ project_id: Optional project ID
540
+ api_key: Optional API key
541
+
542
+ Returns:
543
+ Dict with upload results
544
+ """
545
+ from .primitives import PrimitiveLibrary
546
+
547
+ uploader = SkillLibraryUploader(api_key=api_key)
548
+
549
+ # Get or create project
550
+ if not project_id:
551
+ project_id = uploader.get_or_create_project(
552
+ f"{robot_name}_primitives",
553
+ f"Primitive skill library for {robot_name}",
554
+ )
555
+
556
+ result = {
557
+ "project_id": project_id,
558
+ "artifacts": [],
559
+ }
560
+
561
+ # Create primitive library (without robot for now)
562
+ lib = PrimitiveLibrary(robot_interface=None)
563
+
564
+ # Upload primitives
565
+ for name, prim in lib.primitives.items():
566
+ response = uploader._request("POST", "/artifacts", json={
567
+ "projectId": project_id,
568
+ "name": name,
569
+ "stage": "skill",
570
+ "type": "code",
571
+ "metadata": {
572
+ "robot_name": robot_name,
573
+ "skill_type": "primitive",
574
+ "description": prim.description,
575
+ "servo_targets": {str(k): v for k, v in prim.servo_targets.items()},
576
+ "duration_ms": prim.duration_ms,
577
+ "hardware_requirements": [r.value for r in prim.hardware],
578
+ "dependencies": [],
579
+ "source": "primitives_library",
580
+ },
581
+ })
582
+ result["artifacts"].append({
583
+ "id": response.get("artifact", {}).get("id"),
584
+ "stage": "skill",
585
+ "name": name,
586
+ "skill_type": "primitive",
587
+ })
588
+
589
+ # Upload compound skills
590
+ for name, compound in lib.compounds.items():
591
+ response = uploader._request("POST", "/artifacts", json={
592
+ "projectId": project_id,
593
+ "name": name,
594
+ "stage": "skill",
595
+ "type": "code",
596
+ "metadata": {
597
+ "robot_name": robot_name,
598
+ "skill_type": "compound",
599
+ "description": compound.description,
600
+ "steps": compound.steps,
601
+ "hardware_requirements": [r.value for r in compound.hardware],
602
+ "dependencies": compound.steps, # Each step is a dependency
603
+ "source": "primitives_library",
604
+ },
605
+ })
606
+ result["artifacts"].append({
607
+ "id": response.get("artifact", {}).get("id"),
608
+ "stage": "skill",
609
+ "name": name,
610
+ "skill_type": "compound",
611
+ })
612
+
613
+ # Upload behaviors
614
+ for name, behavior in lib.behaviors.items():
615
+ response = uploader._request("POST", "/artifacts", json={
616
+ "projectId": project_id,
617
+ "name": name,
618
+ "stage": "skill",
619
+ "type": "code",
620
+ "metadata": {
621
+ "robot_name": robot_name,
622
+ "skill_type": "behavior",
623
+ "description": behavior.description,
624
+ "steps": behavior.steps,
625
+ "hardware_requirements": [r.value for r in behavior.hardware],
626
+ "dependencies": [], # Behaviors infer deps from steps
627
+ "requires_perception": True,
628
+ "source": "primitives_library",
629
+ },
630
+ })
631
+ result["artifacts"].append({
632
+ "id": response.get("artifact", {}).get("id"),
633
+ "stage": "skill",
634
+ "name": name,
635
+ "skill_type": "behavior",
636
+ })
637
+
638
+ return result
639
+
640
+
359
641
  def upload_skill_library(
360
642
  robot_name: str,
361
643
  project_id: Optional[str] = None,