Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0cc9f02
feat: add text/image-to-3d-scene pipeline
MuziWong Jun 29, 2026
625802f
Fixed gym export bug: wrong object description;
MuziWong Jun 29, 2026
68adf9a
1. Update prompt in scene_intake; 2. VLM judge percentage of the obje…
MuziWong Jun 29, 2026
8293f2c
Fixed gym export bug;
MuziWong Jun 30, 2026
aafb63b
Cleaned the code;
MuziWong Jul 1, 2026
6a20308
1. Fixed segmentation bug; 2. Fixed image input object position relat…
MuziWong Jul 1, 2026
0da9ddd
Finished scene editing;
MuziWong Jul 1, 2026
4639345
Added a bool button for optimization;
MuziWong Jul 2, 2026
b1594eb
Scene edit with yanjun's SA;
MuziWong Jul 2, 2026
416e4b5
Add a button to choose whether to use the gravity simulation to settl…
MuziWong Jul 3, 2026
2391205
Finished basic scene randomization (with a workflow router)
MuziWong Jul 3, 2026
26732fd
Fixed the bpy bug, using bpy in conda env;
MuziWong Jul 4, 2026
a5c3449
Fixed gym export bug, simplified the pipeline;
MuziWong Jul 4, 2026
4567257
Add timer;
MuziWong Jul 6, 2026
5ef1d24
Fixed the bug in scene editing: generated single mesh scale = [1.0, 1…
MuziWong Jul 6, 2026
9777577
Mesh collision refinement now separates directly related object pairs…
MuziWong Jul 6, 2026
8a80689
1. Fixed the metric scale problem in scene editing; 2. Fixed the supp…
MuziWong Jul 7, 2026
df01b5f
Added max_convex_hull_num = 16 for the tabletop when exporting the gy…
MuziWong Jul 8, 2026
10b43d5
1. Deleted the scene edit/randomization router 2. Added a new feature…
MuziWong Jul 8, 2026
52c0216
1. Fixed the scene-edit output: newly-added object's suffix now chang…
MuziWong Jul 8, 2026
637b8f2
1. Added chenjian bro's bottle z-align algo (include unified_scene_ge…
MuziWong Jul 9, 2026
8a70822
add log when doing optimization
MuziWong Jul 11, 2026
b30a6e7
Using the fallback version of optimization.
MuziWong Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions embodichain/gen_sim/prompt2scene/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
cli/preview*
cli/export*
agent_tools/servers/geometry_generation_server/*

# Python cache
__pycache__/
*.py[cod]
15 changes: 15 additions & 0 deletions embodichain/gen_sim/prompt2scene/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
Comment on lines +1 to +15
1 change: 1 addition & 0 deletions embodichain/gen_sim/prompt2scene/agent_tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Internal client + External server for agent tool calling."""
31 changes: 31 additions & 0 deletions embodichain/gen_sim/prompt2scene/agent_tools/clients/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------

from __future__ import annotations

from embodichain.gen_sim.prompt2scene.agent_tools.clients.base import BaseHttpClient
from embodichain.gen_sim.prompt2scene.agent_tools.clients.common import ClientError
from embodichain.gen_sim.prompt2scene.agent_tools.clients.config import (
DEFAULT_CLIENT_CONFIG_PATH,
load_client_config,
)

__all__ = [
"BaseHttpClient",
"ClientError",
"DEFAULT_CLIENT_CONFIG_PATH",
"load_client_config",
]
131 changes: 131 additions & 0 deletions embodichain/gen_sim/prompt2scene/agent_tools/clients/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------

from __future__ import annotations

import time
from pathlib import Path
from typing import Callable

import requests

from embodichain.gen_sim.prompt2scene.agent_tools.clients.common import (
ClientError,
build_client_error,
)
from embodichain.gen_sim.prompt2scene.agent_tools.clients.config import (
load_client_config,
)
from embodichain.gen_sim.prompt2scene.utils.log import (
log_api_request_start,
log_info,
log_warning,
)

__all__ = ["BaseHttpClient"]


class BaseHttpClient:
"""Shared HTTP client behavior for agent-tool service clients."""

def __init__(
self,
*,
config_key: str,
server_name: str,
base_url: str | None = None,
timeout_s: int | None = None,
config_path: Path | None = None,
session: requests.Session | None = None,
trust_env: bool = True,
) -> None:
"""Initialize common service client fields from config."""
self.config = load_client_config(config_key, config_path)
self.server_name = server_name
self.base_url = (base_url or str(self.config["base_url"])).rstrip("/")
self.timeout_s = int(timeout_s or self.config.get("timeout_s", 120))
self.health_path = str(self.config.get("health_path", "/health"))
self.session = session or requests.Session()
self.session.trust_env = trust_env
log_info(f"{self.server_name} client initialized for {self.base_url}")

def health_check(self) -> bool:
"""Check whether the configured service is healthy."""
try:
response = self.session.get(
f"{self.base_url}{self.health_path}",
timeout=5,
)
response.raise_for_status()
return True
except Exception as exc:
log_warning(f"{self.server_name} health check failed: {exc}")
return False

def post_with_retries(
self,
request_fn: Callable[[], requests.Response],
*,
max_retries: int,
error_cls: type[ClientError] = ClientError,
request_label: str | None = None,
) -> requests.Response | ClientError:
"""Run a POST request function with retry and HTTP error handling."""
for attempt in range(max_retries):
try:
if request_label is not None:
log_api_request_start(
step=self.server_name,
request=request_label,
attempt=attempt + 1,
)
response = request_fn()
response.raise_for_status()
return response

except requests.exceptions.ConnectionError as exc:
if attempt < max_retries - 1:
log_warning(
f"{self.server_name} connection failed; retrying "
f"({attempt + 1}/{max_retries})."
)
time.sleep(min(2**attempt, 60))
continue
raise ConnectionError(
f"Failed to connect to {self.server_name} at {self.base_url}"
) from exc

except requests.exceptions.HTTPError as exc:
response = exc.response
if response is None:
raise RuntimeError(f"{self.server_name} HTTP request failed.") from exc
if response.status_code >= 500 and attempt < max_retries - 1:
log_warning(
f"{self.server_name} server error; retrying "
f"({attempt + 1}/{max_retries})."
)
time.sleep(min(2**attempt, 60))
continue
return build_client_error(
response,
server_name=self.server_name,
error_cls=error_cls,
)

except requests.exceptions.Timeout as exc:
raise TimeoutError(f"{self.server_name} request timed out.") from exc

raise RuntimeError(f"{self.server_name} request failed unexpectedly.")
139 changes: 139 additions & 0 deletions embodichain/gen_sim/prompt2scene/agent_tools/clients/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

import requests

__all__ = [
"ClientError",
"build_client_error",
"first_string",
"format_http_error",
"parse_error_response",
"parse_json_object_response",
"validate_required_strings",
"validate_png_response",
]


@dataclass(frozen=True)
class ClientError:
"""Common HTTP client error response."""

error_message: str
status_code: int | None = None
content_type: str | None = None
headers: dict[str, str] = field(default_factory=dict)
raw_response: dict[str, Any] | None = None


def validate_png_response(
response: requests.Response,
png_bytes: bytes,
) -> None:
content_type = response.headers.get("Content-Type", "")
if "image/png" not in content_type.lower():
raise RuntimeError(
"Image generation server returned non-PNG content: "
f"{content_type or 'unknown'}"
)
if not png_bytes.startswith(b"\x89PNG\r\n\x1a\n"):
raise RuntimeError("Image generation server returned invalid PNG bytes.")


def validate_required_strings(fields: dict[str, object]) -> None:
"""Validate required client request string fields."""
for field_name, value in fields.items():
if not str(value).strip():
raise ValueError(f"{field_name} must be non-empty.")


def format_http_error(response: requests.Response, *, server_name: str) -> str:
"""Format an HTTP error response from an agent-tool server."""
try:
response_data = response.json()
except ValueError:
return f"{server_name} HTTP error: {response.status_code}"

error_message = first_string(
response_data,
"error",
"error_message",
"message",
"detail",
)
if error_message:
return f"{server_name} error: {error_message}"
return f"{server_name} HTTP error: {response.status_code}"


def parse_error_response(response: requests.Response) -> dict[str, Any] | None:
"""Parse an error response body as a JSON object if possible."""
try:
response_data = response.json()
except ValueError:
return None
return response_data if isinstance(response_data, dict) else None


def build_client_error(
response: requests.Response,
*,
server_name: str,
error_cls: type[ClientError] = ClientError,
) -> ClientError:
"""Build a common client error dataclass from an HTTP response."""
return error_cls(
error_message=format_http_error(
response,
server_name=server_name,
),
status_code=response.status_code,
content_type=response.headers.get("Content-Type"),
headers=dict(response.headers),
raw_response=parse_error_response(response),
)


def parse_json_object_response(
response: requests.Response,
*,
server_name: str,
) -> dict[str, Any]:
"""Parse an HTTP response body as a JSON object."""
try:
response_data = response.json()
except ValueError as exc:
raise RuntimeError(
f"{server_name} returned invalid JSON content: "
f"{response.headers.get('Content-Type') or 'unknown'}"
) from exc
if not isinstance(response_data, dict):
raise RuntimeError(f"{server_name} response must be a JSON object.")
return response_data


def first_string(data: dict[str, Any], *keys: str) -> str | None:
"""Return the first string value for the given keys."""
for key in keys:
value = data.get(key)
if isinstance(value, str):
return value
return None
50 changes: 50 additions & 0 deletions embodichain/gen_sim/prompt2scene/agent_tools/clients/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

__all__ = ["DEFAULT_CLIENT_CONFIG_PATH", "load_client_config"]

DEFAULT_CLIENT_CONFIG_PATH = (
Path(__file__).resolve().parents[2] / "configs" / "client_config.json"
)


def load_client_config(
config_key: str,
config_path: Path | None = None,
) -> dict[str, Any]:
"""Load one agent-tool client config section."""
resolved_config_path = (config_path or DEFAULT_CLIENT_CONFIG_PATH).resolve()
if not resolved_config_path.is_file():
raise FileNotFoundError(f"Client config not found: {resolved_config_path}")

with resolved_config_path.open("r", encoding="utf-8") as f:
raw_config = json.load(f)

config = raw_config.get(config_key)
if not isinstance(config, dict):
raise ValueError(
f"Client config section {config_key!r} not found in "
f"{resolved_config_path}"
)
if not config.get("base_url"):
raise ValueError(f"Client config section {config_key!r} requires base_url.")
return config
Loading
Loading