Spaces:
Sleeping
Sleeping
Merge pull request #5 from danghoangnhan/feat/crewai-server-cleanup
Browse files- agents/crew_orchestrator.py +13 -25
- agents/llm_adapter.py +14 -18
- agents/tools.py +56 -0
- server/mcp.py +11 -11
- server/routes.py +4 -2
- server/web.py +13 -4
- tests/test_llm_adapter.py +44 -0
- tests/test_tools.py +45 -0
agents/crew_orchestrator.py
CHANGED
|
@@ -16,15 +16,20 @@ import logging
|
|
| 16 |
import re
|
| 17 |
from pathlib import Path
|
| 18 |
|
|
|
|
| 19 |
from agents.definitions import AGENTS
|
| 20 |
from agents.design_state import DesignState, extract_decisions
|
| 21 |
-
from agents.prompts import CAD_TRIGGER_KEYWORDS, route_by_keywords
|
| 22 |
from agents.orchestrator import _format_response, _execute_cad_code
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
logger = logging.getLogger(__name__)
|
| 25 |
|
| 26 |
DEFAULT_OUTPUT_DIR = Path(__file__).parent.parent / "output"
|
| 27 |
|
|
|
|
|
|
|
| 28 |
|
| 29 |
def _build_agent_context(
|
| 30 |
message: str,
|
|
@@ -57,7 +62,7 @@ def _build_agent_context(
|
|
| 57 |
return "\n\n".join(parts)
|
| 58 |
|
| 59 |
|
| 60 |
-
class CrewOrchestrator:
|
| 61 |
"""Multi-call orchestrator using CrewAI.
|
| 62 |
|
| 63 |
Each selected agent gets its own LLM call with focused context and
|
|
@@ -66,14 +71,9 @@ class CrewOrchestrator:
|
|
| 66 |
Falls back to SingleCallOrchestrator if CrewAI is not installed.
|
| 67 |
"""
|
| 68 |
|
| 69 |
-
def __init__(
|
| 70 |
-
|
| 71 |
-
backend_name: str = "anthropic",
|
| 72 |
-
output_dir: Path | str = DEFAULT_OUTPUT_DIR,
|
| 73 |
-
):
|
| 74 |
self.backend_name = backend_name
|
| 75 |
-
self.output_dir = Path(output_dir)
|
| 76 |
-
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 77 |
self._crew_available = self._check_crewai()
|
| 78 |
|
| 79 |
@staticmethod
|
|
@@ -123,12 +123,12 @@ class CrewOrchestrator:
|
|
| 123 |
if mentions:
|
| 124 |
active_ids = mentions
|
| 125 |
else:
|
| 126 |
-
active_ids =
|
| 127 |
|
| 128 |
# Check CAD trigger
|
| 129 |
include_cad = "cad" in active_ids
|
| 130 |
if not include_cad:
|
| 131 |
-
include_cad =
|
| 132 |
if include_cad and "cad" not in active_ids:
|
| 133 |
active_ids.append("cad")
|
| 134 |
|
|
@@ -263,11 +263,7 @@ class CrewOrchestrator:
|
|
| 263 |
from agents.llm_adapter import NeuralCADLLMAdapter
|
| 264 |
|
| 265 |
backend = self._build_backend()
|
| 266 |
-
model_names =
|
| 267 |
-
"anthropic": "claude-sonnet-4-20250514",
|
| 268 |
-
"openai": "gpt-4o",
|
| 269 |
-
"gemini": "gemini-2.5-flash",
|
| 270 |
-
}
|
| 271 |
return NeuralCADLLMAdapter(
|
| 272 |
backend=backend,
|
| 273 |
model=model_names.get(self.backend_name, "custom"),
|
|
@@ -275,15 +271,7 @@ class CrewOrchestrator:
|
|
| 275 |
|
| 276 |
def _build_backend(self):
|
| 277 |
"""Build the underlying LLM backend."""
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
backends = {
|
| 281 |
-
"anthropic": AnthropicBackend,
|
| 282 |
-
"openai": OpenAIBackend,
|
| 283 |
-
"gemini": GeminiBackend,
|
| 284 |
-
}
|
| 285 |
-
backend_cls = backends.get(self.backend_name, AnthropicBackend)
|
| 286 |
-
return backend_cls()
|
| 287 |
|
| 288 |
# ── Fallback ───────────────────────────────────────────────────────────
|
| 289 |
|
|
|
|
| 16 |
import re
|
| 17 |
from pathlib import Path
|
| 18 |
|
| 19 |
+
from agents.base import BaseOrchestrator
|
| 20 |
from agents.definitions import AGENTS
|
| 21 |
from agents.design_state import DesignState, extract_decisions
|
|
|
|
| 22 |
from agents.orchestrator import _format_response, _execute_cad_code
|
| 23 |
+
from agents.routing import RoutingEngine
|
| 24 |
+
from config.settings import settings
|
| 25 |
+
from core.backend_factory import BackendFactory
|
| 26 |
|
| 27 |
logger = logging.getLogger(__name__)
|
| 28 |
|
| 29 |
DEFAULT_OUTPUT_DIR = Path(__file__).parent.parent / "output"
|
| 30 |
|
| 31 |
+
_router = RoutingEngine()
|
| 32 |
+
|
| 33 |
|
| 34 |
def _build_agent_context(
|
| 35 |
message: str,
|
|
|
|
| 62 |
return "\n\n".join(parts)
|
| 63 |
|
| 64 |
|
| 65 |
+
class CrewOrchestrator(BaseOrchestrator):
|
| 66 |
"""Multi-call orchestrator using CrewAI.
|
| 67 |
|
| 68 |
Each selected agent gets its own LLM call with focused context and
|
|
|
|
| 71 |
Falls back to SingleCallOrchestrator if CrewAI is not installed.
|
| 72 |
"""
|
| 73 |
|
| 74 |
+
def __init__(self, backend_name: str = "anthropic", output_dir=None):
|
| 75 |
+
super().__init__(output_dir=output_dir or DEFAULT_OUTPUT_DIR)
|
|
|
|
|
|
|
|
|
|
| 76 |
self.backend_name = backend_name
|
|
|
|
|
|
|
| 77 |
self._crew_available = self._check_crewai()
|
| 78 |
|
| 79 |
@staticmethod
|
|
|
|
| 123 |
if mentions:
|
| 124 |
active_ids = mentions
|
| 125 |
else:
|
| 126 |
+
active_ids = _router.route(message)
|
| 127 |
|
| 128 |
# Check CAD trigger
|
| 129 |
include_cad = "cad" in active_ids
|
| 130 |
if not include_cad:
|
| 131 |
+
include_cad = _router.has_cad_trigger(message)
|
| 132 |
if include_cad and "cad" not in active_ids:
|
| 133 |
active_ids.append("cad")
|
| 134 |
|
|
|
|
| 263 |
from agents.llm_adapter import NeuralCADLLMAdapter
|
| 264 |
|
| 265 |
backend = self._build_backend()
|
| 266 |
+
model_names = settings.model_for
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
return NeuralCADLLMAdapter(
|
| 268 |
backend=backend,
|
| 269 |
model=model_names.get(self.backend_name, "custom"),
|
|
|
|
| 271 |
|
| 272 |
def _build_backend(self):
|
| 273 |
"""Build the underlying LLM backend."""
|
| 274 |
+
return BackendFactory.create(self.backend_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
# ── Fallback ───────────────────────────────────────────────────────────
|
| 277 |
|
agents/llm_adapter.py
CHANGED
|
@@ -1,28 +1,22 @@
|
|
| 1 |
-
"""CrewAI
|
| 2 |
|
| 3 |
from __future__ import annotations
|
|
|
|
|
|
|
| 4 |
from typing import Any
|
| 5 |
|
|
|
|
|
|
|
| 6 |
try:
|
| 7 |
-
from crewai import
|
| 8 |
except ImportError:
|
| 9 |
-
#
|
| 10 |
-
class BaseLLM:
|
| 11 |
-
def __init__(self,
|
| 12 |
-
self.model = model
|
| 13 |
-
def call(self, messages, **kwargs) -> str:
|
| 14 |
-
raise NotImplementedError
|
| 15 |
|
| 16 |
|
| 17 |
class NeuralCADLLMAdapter(BaseLLM):
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
Usage:
|
| 21 |
-
from core.backends import GeminiBackend
|
| 22 |
-
backend = GeminiBackend()
|
| 23 |
-
adapter = NeuralCADLLMAdapter(backend, model="gemini-2.5-flash")
|
| 24 |
-
# Now usable as CrewAI agent's llm parameter
|
| 25 |
-
"""
|
| 26 |
|
| 27 |
def __init__(self, backend, model: str = "custom", **kwargs):
|
| 28 |
super().__init__(model=model, **kwargs)
|
|
@@ -36,13 +30,15 @@ class NeuralCADLLMAdapter(BaseLLM):
|
|
| 36 |
available_functions: Any = None,
|
| 37 |
**kwargs,
|
| 38 |
) -> str:
|
| 39 |
-
# If messages is a string, wrap it in standard format
|
| 40 |
if isinstance(messages, str):
|
| 41 |
messages = [{"role": "user", "content": messages}]
|
| 42 |
return self.backend.generate(messages)
|
| 43 |
|
| 44 |
def supports_function_calling(self) -> bool:
|
| 45 |
-
return
|
| 46 |
|
| 47 |
def supports_stop_words(self) -> bool:
|
| 48 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CrewAI LLM adapter — wraps NeuralCAD's LLMBackend for CrewAI compatibility."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
from typing import Any
|
| 7 |
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
try:
|
| 11 |
+
from crewai.llm import BaseLLM
|
| 12 |
except ImportError:
|
| 13 |
+
# Stub if crewai not installed
|
| 14 |
+
class BaseLLM: # type: ignore[no-redef]
|
| 15 |
+
def __init__(self, **kwargs): pass
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
class NeuralCADLLMAdapter(BaseLLM):
|
| 19 |
+
"""Wraps NeuralCAD's LLMBackend for CrewAI agent usage."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
def __init__(self, backend, model: str = "custom", **kwargs):
|
| 22 |
super().__init__(model=model, **kwargs)
|
|
|
|
| 30 |
available_functions: Any = None,
|
| 31 |
**kwargs,
|
| 32 |
) -> str:
|
|
|
|
| 33 |
if isinstance(messages, str):
|
| 34 |
messages = [{"role": "user", "content": messages}]
|
| 35 |
return self.backend.generate(messages)
|
| 36 |
|
| 37 |
def supports_function_calling(self) -> bool:
|
| 38 |
+
return True # Enable CrewAI tool usage
|
| 39 |
|
| 40 |
def supports_stop_words(self) -> bool:
|
| 41 |
return False
|
| 42 |
+
|
| 43 |
+
def supports_vision(self) -> bool:
|
| 44 |
+
return hasattr(self.backend, 'generate_with_image')
|
agents/tools.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CrewAI tools for CadQuery code execution and CNC validation.
|
| 2 |
+
|
| 3 |
+
These tools allow the CAD Coder agent to execute code and validate
|
| 4 |
+
manufacturability within its reasoning loop, enabling self-correction.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import logging
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from crewai.tools import tool
|
| 16 |
+
except ImportError:
|
| 17 |
+
# Stub if crewai not installed — tools won't be available
|
| 18 |
+
def tool(name):
|
| 19 |
+
def decorator(func):
|
| 20 |
+
return func
|
| 21 |
+
return decorator
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@tool("Execute CadQuery Code")
|
| 25 |
+
def execute_cad_tool(code: str) -> str:
|
| 26 |
+
"""Execute CadQuery Python code and return geometry info.
|
| 27 |
+
|
| 28 |
+
The code must assign result to a variable called `result` as cq.Workplane.
|
| 29 |
+
Returns JSON with: success, volume_mm3, bounding_box_mm, face_count, edge_count, error.
|
| 30 |
+
"""
|
| 31 |
+
from core.executor import execute_cadquery
|
| 32 |
+
from core.serializers import ExecutionResultSerializer
|
| 33 |
+
|
| 34 |
+
result = execute_cadquery(code)
|
| 35 |
+
return json.dumps(ExecutionResultSerializer.to_dict(result), indent=2)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@tool("Validate CNC Manufacturability")
|
| 39 |
+
def validate_cad_tool(code: str) -> str:
|
| 40 |
+
"""Run CNC manufacturability checks on CadQuery code.
|
| 41 |
+
|
| 42 |
+
Returns JSON with: machinable, axis_recommendation, issues list.
|
| 43 |
+
"""
|
| 44 |
+
from core.executor import execute_cadquery
|
| 45 |
+
from core.validator import validate_for_cnc
|
| 46 |
+
from core.serializers import ExecutionResultSerializer, ValidationResultSerializer
|
| 47 |
+
|
| 48 |
+
exec_result = execute_cadquery(code)
|
| 49 |
+
if not exec_result.success:
|
| 50 |
+
return json.dumps({"success": False, "error": exec_result.error})
|
| 51 |
+
|
| 52 |
+
validation = validate_for_cnc(exec_result.result)
|
| 53 |
+
return json.dumps({
|
| 54 |
+
"execution": ExecutionResultSerializer.to_dict(exec_result),
|
| 55 |
+
"validation": ValidationResultSerializer.to_dict(validation),
|
| 56 |
+
}, indent=2)
|
server/mcp.py
CHANGED
|
@@ -37,26 +37,26 @@ mcp = FastMCP(
|
|
| 37 |
),
|
| 38 |
)
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
DEFAULT_OUTPUT_DIR.mkdir(exist_ok=True)
|
| 42 |
|
| 43 |
|
| 44 |
# ── Helper: LLM Backend Selection ────────────────────────────────────────
|
| 45 |
|
| 46 |
def get_backend(backend_name: str = "mock"):
|
| 47 |
-
"""Get
|
| 48 |
-
from core.backends import MockBackend, AnthropicBackend, OpenAIBackend, GeminiBackend, NeuralCADBackend
|
| 49 |
|
|
|
|
|
|
|
| 50 |
if backend_name == "neural":
|
|
|
|
| 51 |
return NeuralCADBackend()
|
| 52 |
-
|
| 53 |
-
return AnthropicBackend()
|
| 54 |
-
elif backend_name == "openai" and os.environ.get("OPENAI_API_KEY"):
|
| 55 |
-
return OpenAIBackend()
|
| 56 |
-
elif backend_name == "gemini" and os.environ.get("GEMINI_API_KEY"):
|
| 57 |
-
return GeminiBackend()
|
| 58 |
-
else:
|
| 59 |
-
return MockBackend()
|
| 60 |
|
| 61 |
|
| 62 |
# ── Tool: generate_cnc_model ─────────────────────────────────────────────
|
|
|
|
| 37 |
),
|
| 38 |
)
|
| 39 |
|
| 40 |
+
from config.settings import settings
|
| 41 |
+
from core.backend_factory import BackendFactory
|
| 42 |
+
|
| 43 |
+
DEFAULT_OUTPUT_DIR = settings.output_dir
|
| 44 |
+
if not DEFAULT_OUTPUT_DIR.is_absolute():
|
| 45 |
+
DEFAULT_OUTPUT_DIR = Path(__file__).parent.parent / DEFAULT_OUTPUT_DIR
|
| 46 |
DEFAULT_OUTPUT_DIR.mkdir(exist_ok=True)
|
| 47 |
|
| 48 |
|
| 49 |
# ── Helper: LLM Backend Selection ────────────────────────────────────────
|
| 50 |
|
| 51 |
def get_backend(backend_name: str = "mock"):
|
| 52 |
+
"""Get LLM backend by name, using the factory registry.
|
|
|
|
| 53 |
|
| 54 |
+
NeuralCADBackend is a special case not registered in the factory.
|
| 55 |
+
"""
|
| 56 |
if backend_name == "neural":
|
| 57 |
+
from core.backends import NeuralCADBackend
|
| 58 |
return NeuralCADBackend()
|
| 59 |
+
return BackendFactory.create_safe(backend_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
# ── Tool: generate_cnc_model ─────────────────────────────────────────────
|
server/routes.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from pathlib import Path
|
| 6 |
-
from typing import Optional
|
| 7 |
|
| 8 |
from fastapi import APIRouter
|
| 9 |
from fastapi.responses import JSONResponse
|
|
@@ -13,10 +12,13 @@ from agents.orchestrator import get_orchestrator
|
|
| 13 |
from agents.crew_orchestrator import CrewOrchestrator
|
| 14 |
from agents.prompts import parse_mentions
|
| 15 |
from agents.definitions import AGENTS
|
|
|
|
| 16 |
|
| 17 |
router = APIRouter()
|
| 18 |
|
| 19 |
-
OUTPUT_DIR =
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
# ── Request / response models ─────────────────────────────────────────────
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from pathlib import Path
|
|
|
|
| 6 |
|
| 7 |
from fastapi import APIRouter
|
| 8 |
from fastapi.responses import JSONResponse
|
|
|
|
| 12 |
from agents.crew_orchestrator import CrewOrchestrator
|
| 13 |
from agents.prompts import parse_mentions
|
| 14 |
from agents.definitions import AGENTS
|
| 15 |
+
from config.settings import settings
|
| 16 |
|
| 17 |
router = APIRouter()
|
| 18 |
|
| 19 |
+
OUTPUT_DIR = settings.output_dir
|
| 20 |
+
if not OUTPUT_DIR.is_absolute():
|
| 21 |
+
OUTPUT_DIR = Path(__file__).parent.parent / OUTPUT_DIR
|
| 22 |
|
| 23 |
|
| 24 |
# ── Request / response models ─────────────────────────────────────────────
|
server/web.py
CHANGED
|
@@ -38,10 +38,19 @@ from mcp.client.sse import sse_client
|
|
| 38 |
|
| 39 |
# ── Config ───────────────────────────────────────────────────────────────
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
# ── MCP Client Management ───────────────────────────────────────────────
|
| 47 |
|
|
|
|
| 38 |
|
| 39 |
# ── Config ───────────────────────────────────────────────────────────────
|
| 40 |
|
| 41 |
+
from config.settings import settings
|
| 42 |
+
|
| 43 |
+
OUTPUT_DIR = settings.output_dir
|
| 44 |
+
if not OUTPUT_DIR.is_absolute():
|
| 45 |
+
OUTPUT_DIR = Path(__file__).parent.parent / OUTPUT_DIR
|
| 46 |
+
|
| 47 |
+
WEB_DIR = Path(settings.paths.get("web_dir", "./web"))
|
| 48 |
+
if not WEB_DIR.is_absolute():
|
| 49 |
+
WEB_DIR = Path(__file__).parent.parent / WEB_DIR
|
| 50 |
+
|
| 51 |
+
PORT = settings.web_port
|
| 52 |
+
|
| 53 |
+
MCP_SERVER_URL = os.environ.get("MCP_SERVER_URL", f"http://localhost:{settings.mcp_port}/sse")
|
| 54 |
|
| 55 |
# ── MCP Client Management ───────────────────────────────────────────────
|
| 56 |
|
tests/test_llm_adapter.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for agents/llm_adapter.py."""
|
| 2 |
+
|
| 3 |
+
from agents.llm_adapter import NeuralCADLLMAdapter
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class FakeBackend:
|
| 7 |
+
def generate(self, messages):
|
| 8 |
+
return "test response"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class FakeVisionBackend:
|
| 12 |
+
def generate(self, messages):
|
| 13 |
+
return "test response"
|
| 14 |
+
|
| 15 |
+
def generate_with_image(self, messages, image_path):
|
| 16 |
+
return "vision response"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class TestNeuralCADLLMAdapter:
|
| 20 |
+
def test_call_with_messages(self):
|
| 21 |
+
adapter = NeuralCADLLMAdapter(FakeBackend())
|
| 22 |
+
result = adapter.call([{"role": "user", "content": "hello"}])
|
| 23 |
+
assert result == "test response"
|
| 24 |
+
|
| 25 |
+
def test_call_with_string(self):
|
| 26 |
+
adapter = NeuralCADLLMAdapter(FakeBackend())
|
| 27 |
+
result = adapter.call("hello")
|
| 28 |
+
assert result == "test response"
|
| 29 |
+
|
| 30 |
+
def test_supports_function_calling(self):
|
| 31 |
+
adapter = NeuralCADLLMAdapter(FakeBackend())
|
| 32 |
+
assert adapter.supports_function_calling() is True
|
| 33 |
+
|
| 34 |
+
def test_supports_stop_words(self):
|
| 35 |
+
adapter = NeuralCADLLMAdapter(FakeBackend())
|
| 36 |
+
assert adapter.supports_stop_words() is False
|
| 37 |
+
|
| 38 |
+
def test_supports_vision_false(self):
|
| 39 |
+
adapter = NeuralCADLLMAdapter(FakeBackend())
|
| 40 |
+
assert adapter.supports_vision() is False
|
| 41 |
+
|
| 42 |
+
def test_supports_vision_true(self):
|
| 43 |
+
adapter = NeuralCADLLMAdapter(FakeVisionBackend())
|
| 44 |
+
assert adapter.supports_vision() is True
|
tests/test_tools.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for agents/tools.py — CrewAI CadQuery tools.
|
| 2 |
+
|
| 3 |
+
These tests require CadQuery to be installed.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import pytest
|
| 8 |
+
|
| 9 |
+
from agents.tools import execute_cad_tool, validate_cad_tool
|
| 10 |
+
|
| 11 |
+
pytestmark = pytest.mark.requires_cadquery
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TestExecuteCadTool:
|
| 15 |
+
def test_valid_code(self):
|
| 16 |
+
result = json.loads(execute_cad_tool.run("result = cq.Workplane('XY').box(10,10,10)"))
|
| 17 |
+
assert result["success"] is True
|
| 18 |
+
assert result["volume_mm3"] > 0
|
| 19 |
+
assert result["face_count"] == 6
|
| 20 |
+
|
| 21 |
+
def test_invalid_code(self):
|
| 22 |
+
result = json.loads(execute_cad_tool.run("result = bad_code"))
|
| 23 |
+
assert result["success"] is False
|
| 24 |
+
assert result["error"] is not None
|
| 25 |
+
|
| 26 |
+
def test_missing_result(self):
|
| 27 |
+
result = json.loads(execute_cad_tool.run("x = 42"))
|
| 28 |
+
assert result["success"] is False
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class TestValidateCadTool:
|
| 32 |
+
def test_valid_machinable(self):
|
| 33 |
+
result = json.loads(validate_cad_tool.run("result = cq.Workplane('XY').box(50, 30, 10)"))
|
| 34 |
+
assert "validation" in result
|
| 35 |
+
assert result["validation"]["machinable"] is True
|
| 36 |
+
|
| 37 |
+
def test_invalid_code(self):
|
| 38 |
+
result = json.loads(validate_cad_tool.run("result = bad_code"))
|
| 39 |
+
assert result["success"] is False
|
| 40 |
+
|
| 41 |
+
def test_returns_execution_and_validation(self):
|
| 42 |
+
result = json.loads(validate_cad_tool.run("result = cq.Workplane('XY').box(20, 20, 20)"))
|
| 43 |
+
assert "execution" in result
|
| 44 |
+
assert "validation" in result
|
| 45 |
+
assert result["execution"]["face_count"] == 6
|