Spaces:
Sleeping
Sleeping
Commit ·
d46cdb3
1
Parent(s): edf10d0
feat: add DesignState memory + localStorage persistence
Browse files- agents/design_state.py: Pydantic DesignState model with regex-based
extraction of materials, dimensions, fasteners, axis recommendations,
and design decisions from agent messages (no extra LLM call)
- agents/prompts.py: inject "Current Design Spec" block into LLM
context so decisions survive history truncation
- agents/orchestrator.py: both orchestrators maintain and return
design_state across turns
- server/routes.py: pass design_state through API
- web/index.html: localStorage persistence for chat history and
design state, restore on page load, "NEW" button to start fresh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- agents/crew_orchestrator.py +6 -8
- agents/design_state.py +175 -0
- agents/orchestrator.py +18 -4
- agents/prompts.py +9 -8
- server/routes.py +2 -0
- web/index.html +71 -0
agents/crew_orchestrator.py
CHANGED
|
@@ -10,10 +10,7 @@ Note: Falls back to SingleCallOrchestrator if CrewAI is not installed.
|
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
from pathlib import Path
|
| 13 |
-
from typing import Optional
|
| 14 |
|
| 15 |
-
from agents.definitions import AGENTS, AGENT_COLORS, AGENT_NAMES, AGENT_AVATARS
|
| 16 |
-
from agents.prompts import route_by_keywords
|
| 17 |
|
| 18 |
|
| 19 |
DEFAULT_OUTPUT_DIR = Path(__file__).parent.parent / "output"
|
|
@@ -46,13 +43,14 @@ class CrewOrchestrator:
|
|
| 46 |
history: list[dict],
|
| 47 |
mentions: list[str] | None = None,
|
| 48 |
max_history: int = 30,
|
|
|
|
| 49 |
) -> dict:
|
| 50 |
"""Run one chat turn using CrewAI multi-call process.
|
| 51 |
|
| 52 |
Falls back to SingleCallOrchestrator if CrewAI is not available.
|
| 53 |
|
| 54 |
Returns same format as SingleCallOrchestrator:
|
| 55 |
-
{"responses": [...], "preview": None | {...}}
|
| 56 |
"""
|
| 57 |
if not self._crew_available:
|
| 58 |
# Fallback to single-call
|
|
@@ -67,12 +65,12 @@ class CrewOrchestrator:
|
|
| 67 |
except Exception:
|
| 68 |
from agents.orchestrator import MockChatBackend
|
| 69 |
mock = MockChatBackend()
|
| 70 |
-
return mock.chat_turn(message, history, mentions)
|
| 71 |
|
| 72 |
orchestrator = SingleCallOrchestrator(
|
| 73 |
backend=backend, output_dir=self.output_dir
|
| 74 |
)
|
| 75 |
-
return orchestrator.chat_turn(message, history, mentions, max_history)
|
| 76 |
|
| 77 |
# TODO: Implement CrewAI hierarchical process
|
| 78 |
# For now, delegate to single-call as well
|
|
@@ -87,9 +85,9 @@ class CrewOrchestrator:
|
|
| 87 |
except Exception:
|
| 88 |
from agents.orchestrator import MockChatBackend
|
| 89 |
mock = MockChatBackend()
|
| 90 |
-
return mock.chat_turn(message, history, mentions)
|
| 91 |
|
| 92 |
orchestrator = SingleCallOrchestrator(
|
| 93 |
backend=backend, output_dir=self.output_dir
|
| 94 |
)
|
| 95 |
-
return orchestrator.chat_turn(message, history, mentions, max_history)
|
|
|
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
from pathlib import Path
|
|
|
|
| 13 |
|
|
|
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
DEFAULT_OUTPUT_DIR = Path(__file__).parent.parent / "output"
|
|
|
|
| 43 |
history: list[dict],
|
| 44 |
mentions: list[str] | None = None,
|
| 45 |
max_history: int = 30,
|
| 46 |
+
design_state: dict | None = None,
|
| 47 |
) -> dict:
|
| 48 |
"""Run one chat turn using CrewAI multi-call process.
|
| 49 |
|
| 50 |
Falls back to SingleCallOrchestrator if CrewAI is not available.
|
| 51 |
|
| 52 |
Returns same format as SingleCallOrchestrator:
|
| 53 |
+
{"responses": [...], "preview": None | {...}, "design_state": {...}}
|
| 54 |
"""
|
| 55 |
if not self._crew_available:
|
| 56 |
# Fallback to single-call
|
|
|
|
| 65 |
except Exception:
|
| 66 |
from agents.orchestrator import MockChatBackend
|
| 67 |
mock = MockChatBackend()
|
| 68 |
+
return mock.chat_turn(message, history, mentions, design_state=design_state)
|
| 69 |
|
| 70 |
orchestrator = SingleCallOrchestrator(
|
| 71 |
backend=backend, output_dir=self.output_dir
|
| 72 |
)
|
| 73 |
+
return orchestrator.chat_turn(message, history, mentions, max_history, design_state=design_state)
|
| 74 |
|
| 75 |
# TODO: Implement CrewAI hierarchical process
|
| 76 |
# For now, delegate to single-call as well
|
|
|
|
| 85 |
except Exception:
|
| 86 |
from agents.orchestrator import MockChatBackend
|
| 87 |
mock = MockChatBackend()
|
| 88 |
+
return mock.chat_turn(message, history, mentions, design_state=design_state)
|
| 89 |
|
| 90 |
orchestrator = SingleCallOrchestrator(
|
| 91 |
backend=backend, output_dir=self.output_dir
|
| 92 |
)
|
| 93 |
+
return orchestrator.chat_turn(message, history, mentions, max_history, design_state=design_state)
|
agents/design_state.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Design state accumulator — extracts and persists key decisions from agent messages."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
from pydantic import BaseModel, Field
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class DesignState(BaseModel):
|
| 10 |
+
"""Structured state tracking design decisions across chat turns."""
|
| 11 |
+
part_name: str = ""
|
| 12 |
+
description: str = ""
|
| 13 |
+
material: str = ""
|
| 14 |
+
dimensions: dict[str, float] = Field(default_factory=dict)
|
| 15 |
+
features: list[str] = Field(default_factory=list)
|
| 16 |
+
constraints: list[str] = Field(default_factory=list)
|
| 17 |
+
decisions: list[str] = Field(default_factory=list)
|
| 18 |
+
axis_recommendation: str = ""
|
| 19 |
+
|
| 20 |
+
def render(self) -> str:
|
| 21 |
+
"""Render non-empty fields as a concise spec block for LLM context."""
|
| 22 |
+
lines = []
|
| 23 |
+
if self.part_name:
|
| 24 |
+
lines.append(f"Part: {self.part_name}")
|
| 25 |
+
if self.description:
|
| 26 |
+
lines.append(f"Description: {self.description}")
|
| 27 |
+
if self.material:
|
| 28 |
+
lines.append(f"Material: {self.material}")
|
| 29 |
+
if self.dimensions:
|
| 30 |
+
dims = ", ".join(f"{k}={v}mm" for k, v in self.dimensions.items())
|
| 31 |
+
lines.append(f"Dimensions: {dims}")
|
| 32 |
+
if self.features:
|
| 33 |
+
lines.append(f"Features: {'; '.join(self.features)}")
|
| 34 |
+
if self.constraints:
|
| 35 |
+
lines.append(f"Constraints: {'; '.join(self.constraints)}")
|
| 36 |
+
if self.axis_recommendation:
|
| 37 |
+
lines.append(f"Axis: {self.axis_recommendation}")
|
| 38 |
+
if self.decisions:
|
| 39 |
+
lines.append("Decisions:")
|
| 40 |
+
for d in self.decisions[-5:]: # Last 5 decisions to keep it concise
|
| 41 |
+
lines.append(f" - {d}")
|
| 42 |
+
return "\n".join(lines) if lines else ""
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ── Material patterns ──────────────────────────────────────────────────────
|
| 46 |
+
|
| 47 |
+
_MATERIALS = [
|
| 48 |
+
"aluminum", "aluminium", "steel", "stainless steel", "brass", "copper",
|
| 49 |
+
"titanium", "nylon", "delrin", "acetal", "abs", "polycarbonate", "peek",
|
| 50 |
+
]
|
| 51 |
+
_MATERIAL_GRADES = {
|
| 52 |
+
"6061": "aluminum 6061", "7075": "aluminum 7075",
|
| 53 |
+
"304": "stainless steel 304", "316": "stainless steel 316",
|
| 54 |
+
"t6": "aluminum 6061-T6",
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# ── Dimension context words ────────────────────────────────────────────────
|
| 58 |
+
|
| 59 |
+
_DIM_CONTEXTS = {
|
| 60 |
+
"wide": "width", "width": "width",
|
| 61 |
+
"tall": "height", "height": "height", "high": "height",
|
| 62 |
+
"thick": "thickness", "thickness": "thickness",
|
| 63 |
+
"deep": "depth", "depth": "depth",
|
| 64 |
+
"long": "length", "length": "length",
|
| 65 |
+
"diameter": "diameter", "dia": "diameter",
|
| 66 |
+
"radius": "radius",
|
| 67 |
+
"arm": "arm_length",
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def extract_decisions(
|
| 72 |
+
agent_responses: list[dict],
|
| 73 |
+
current_state: DesignState,
|
| 74 |
+
user_message: str = "",
|
| 75 |
+
) -> DesignState:
|
| 76 |
+
"""Extract design decisions from agent responses and update state.
|
| 77 |
+
|
| 78 |
+
Uses regex/keyword matching — no extra LLM call.
|
| 79 |
+
"""
|
| 80 |
+
state = current_state.model_copy(deep=True)
|
| 81 |
+
|
| 82 |
+
# Combine all text for scanning
|
| 83 |
+
all_text = user_message + " " + " ".join(r.get("message", "") for r in agent_responses)
|
| 84 |
+
lower = all_text.lower()
|
| 85 |
+
|
| 86 |
+
# Extract material
|
| 87 |
+
for grade, full_name in _MATERIAL_GRADES.items():
|
| 88 |
+
if grade in lower:
|
| 89 |
+
state.material = full_name
|
| 90 |
+
break
|
| 91 |
+
else:
|
| 92 |
+
for mat in _MATERIALS:
|
| 93 |
+
if mat in lower:
|
| 94 |
+
state.material = mat
|
| 95 |
+
break
|
| 96 |
+
|
| 97 |
+
# Extract dimensions: "60mm wide", "width of 60mm", "60 mm thick"
|
| 98 |
+
dim_pattern = re.compile(
|
| 99 |
+
r'(\d+\.?\d*)\s*mm\s+(' + '|'.join(_DIM_CONTEXTS.keys()) + r')',
|
| 100 |
+
re.IGNORECASE,
|
| 101 |
+
)
|
| 102 |
+
for match in dim_pattern.finditer(all_text):
|
| 103 |
+
value = float(match.group(1))
|
| 104 |
+
word = match.group(2).lower()
|
| 105 |
+
dim_name = _DIM_CONTEXTS.get(word, word)
|
| 106 |
+
state.dimensions[dim_name] = value
|
| 107 |
+
|
| 108 |
+
# Also match "width: 60mm" or "width of 60mm" patterns
|
| 109 |
+
dim_pattern2 = re.compile(
|
| 110 |
+
r'(' + '|'.join(_DIM_CONTEXTS.keys()) + r')\s*(?:of|:|\s)\s*(\d+\.?\d*)\s*mm',
|
| 111 |
+
re.IGNORECASE,
|
| 112 |
+
)
|
| 113 |
+
for match in dim_pattern2.finditer(all_text):
|
| 114 |
+
word = match.group(1).lower()
|
| 115 |
+
value = float(match.group(2))
|
| 116 |
+
dim_name = _DIM_CONTEXTS.get(word, word)
|
| 117 |
+
state.dimensions[dim_name] = value
|
| 118 |
+
|
| 119 |
+
# Extract fastener features: "4x M6 holes", "M4 clearance holes"
|
| 120 |
+
fastener_pattern = re.compile(r'(\d+)\s*[x\u00d7]\s*(M\d+)\s+\w*\s*hole', re.IGNORECASE)
|
| 121 |
+
for match in fastener_pattern.finditer(all_text):
|
| 122 |
+
feature = f"{match.group(1)}x {match.group(2).upper()} holes"
|
| 123 |
+
if feature not in state.features:
|
| 124 |
+
state.features.append(feature)
|
| 125 |
+
|
| 126 |
+
# Single fastener mention: "M6 holes", "M3 clearance holes"
|
| 127 |
+
single_fastener = re.compile(r'(M\d+)\s+(?:clearance\s+)?(?:hole|bolt|screw)', re.IGNORECASE)
|
| 128 |
+
for match in single_fastener.finditer(all_text):
|
| 129 |
+
feature = f"{match.group(1).upper()} holes"
|
| 130 |
+
if feature not in state.features and not any(feature.split()[0] in f for f in state.features):
|
| 131 |
+
state.features.append(feature)
|
| 132 |
+
|
| 133 |
+
# Extract axis recommendation
|
| 134 |
+
axis_pattern = re.compile(r'(3-axis|3\+2[\s-]*axis|5-axis)', re.IGNORECASE)
|
| 135 |
+
axis_match = axis_pattern.search(all_text)
|
| 136 |
+
if axis_match:
|
| 137 |
+
state.axis_recommendation = axis_match.group(1).lower()
|
| 138 |
+
|
| 139 |
+
# Extract constraint keywords
|
| 140 |
+
constraint_patterns = [
|
| 141 |
+
(r'min(?:imum)?\s+wall\s+(?:thickness\s+)?(\d+\.?\d*)\s*mm', "min wall {}mm"),
|
| 142 |
+
(r'max(?:imum)?\s+(?:part\s+)?size\s+(\d+\.?\d*)\s*mm', "max size {}mm"),
|
| 143 |
+
]
|
| 144 |
+
for pattern, template in constraint_patterns:
|
| 145 |
+
match = re.search(pattern, all_text, re.IGNORECASE)
|
| 146 |
+
if match:
|
| 147 |
+
constraint = template.format(match.group(1))
|
| 148 |
+
if constraint not in state.constraints:
|
| 149 |
+
state.constraints.append(constraint)
|
| 150 |
+
|
| 151 |
+
# Extract decisions: sentences with agreement language from agent messages only
|
| 152 |
+
for resp in agent_responses:
|
| 153 |
+
msg = resp.get("message", "")
|
| 154 |
+
sentences = re.split(r'[.!?]+', msg)
|
| 155 |
+
for sentence in sentences:
|
| 156 |
+
s = sentence.strip()
|
| 157 |
+
if len(s) > 15 and any(kw in s.lower() for kw in [
|
| 158 |
+
"recommend", "suggest", "should use", "let's go with",
|
| 159 |
+
"i'd use", "best to", "we'll need", "i'll specify",
|
| 160 |
+
]):
|
| 161 |
+
if s not in state.decisions and len(state.decisions) < 20:
|
| 162 |
+
state.decisions.append(s)
|
| 163 |
+
|
| 164 |
+
# Extract part name from user message if not set
|
| 165 |
+
if not state.part_name and user_message:
|
| 166 |
+
name_patterns = [
|
| 167 |
+
r'(?:need|want|design|make|create)\s+(?:a|an)\s+(.{5,40?})\s*(?:with|for|that|,|$)',
|
| 168 |
+
]
|
| 169 |
+
for pattern in name_patterns:
|
| 170 |
+
match = re.search(pattern, user_message, re.IGNORECASE)
|
| 171 |
+
if match:
|
| 172 |
+
state.part_name = match.group(1).strip()
|
| 173 |
+
break
|
| 174 |
+
|
| 175 |
+
return state
|
agents/orchestrator.py
CHANGED
|
@@ -25,6 +25,7 @@ from agents.prompts import (
|
|
| 25 |
parse_orchestrator_response,
|
| 26 |
CAD_TRIGGER_KEYWORDS,
|
| 27 |
)
|
|
|
|
| 28 |
from core.backends import LLMBackend, MockBackend
|
| 29 |
from core.executor import execute_cadquery, export_all
|
| 30 |
from core.validator import validate_for_cnc
|
|
@@ -154,8 +155,10 @@ class MockChatBackend:
|
|
| 154 |
history: list[dict],
|
| 155 |
mentions: list[str] | None = None,
|
| 156 |
max_history: int = 30,
|
|
|
|
| 157 |
) -> dict:
|
| 158 |
-
"""Return ``{"responses": [...], "preview": ...}``."""
|
|
|
|
| 159 |
lower = message.lower()
|
| 160 |
|
| 161 |
# Determine which agents respond
|
|
@@ -197,7 +200,10 @@ class MockChatBackend:
|
|
| 197 |
)
|
| 198 |
preview = _execute_cad_code(code, message, self.output_dir)
|
| 199 |
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
# -- canned response templates -------------------------------------------
|
| 203 |
|
|
@@ -283,6 +289,7 @@ class SingleCallOrchestrator:
|
|
| 283 |
history: list[dict],
|
| 284 |
mentions: list[str] | None = None,
|
| 285 |
max_history: int = 30,
|
|
|
|
| 286 |
) -> dict:
|
| 287 |
"""Run one chat turn: user message -> agent responses.
|
| 288 |
|
|
@@ -291,10 +298,13 @@ class SingleCallOrchestrator:
|
|
| 291 |
history: Previous messages [{role, agent_id, content}, ...].
|
| 292 |
mentions: Agent IDs explicitly mentioned by user. ``None`` = auto-route.
|
| 293 |
max_history: Max history messages to include in context.
|
|
|
|
| 294 |
|
| 295 |
Returns:
|
| 296 |
-
``{"responses": [...], "preview": None | {...}}``
|
| 297 |
"""
|
|
|
|
|
|
|
| 298 |
# Determine which agents are active
|
| 299 |
active_agents = mentions if mentions else None # None lets orchestrator decide
|
| 300 |
|
|
@@ -315,6 +325,7 @@ class SingleCallOrchestrator:
|
|
| 315 |
history=history,
|
| 316 |
system_prompt=system_prompt,
|
| 317 |
max_history=max_history,
|
|
|
|
| 318 |
)
|
| 319 |
|
| 320 |
# Single LLM call
|
|
@@ -350,7 +361,10 @@ class SingleCallOrchestrator:
|
|
| 350 |
resp["code"], message, self.output_dir, backend=self.backend,
|
| 351 |
)
|
| 352 |
|
| 353 |
-
|
|
|
|
|
|
|
|
|
|
| 354 |
|
| 355 |
|
| 356 |
# ---------------------------------------------------------------------------
|
|
|
|
| 25 |
parse_orchestrator_response,
|
| 26 |
CAD_TRIGGER_KEYWORDS,
|
| 27 |
)
|
| 28 |
+
from agents.design_state import DesignState, extract_decisions
|
| 29 |
from core.backends import LLMBackend, MockBackend
|
| 30 |
from core.executor import execute_cadquery, export_all
|
| 31 |
from core.validator import validate_for_cnc
|
|
|
|
| 155 |
history: list[dict],
|
| 156 |
mentions: list[str] | None = None,
|
| 157 |
max_history: int = 30,
|
| 158 |
+
design_state: dict | None = None,
|
| 159 |
) -> dict:
|
| 160 |
+
"""Return ``{"responses": [...], "preview": ..., "design_state": ...}``."""
|
| 161 |
+
state = DesignState(**(design_state or {}))
|
| 162 |
lower = message.lower()
|
| 163 |
|
| 164 |
# Determine which agents respond
|
|
|
|
| 200 |
)
|
| 201 |
preview = _execute_cad_code(code, message, self.output_dir)
|
| 202 |
|
| 203 |
+
# Update design state from responses
|
| 204 |
+
updated_state = extract_decisions(responses, state, message)
|
| 205 |
+
|
| 206 |
+
return {"responses": responses, "preview": preview, "design_state": updated_state.model_dump()}
|
| 207 |
|
| 208 |
# -- canned response templates -------------------------------------------
|
| 209 |
|
|
|
|
| 289 |
history: list[dict],
|
| 290 |
mentions: list[str] | None = None,
|
| 291 |
max_history: int = 30,
|
| 292 |
+
design_state: dict | None = None,
|
| 293 |
) -> dict:
|
| 294 |
"""Run one chat turn: user message -> agent responses.
|
| 295 |
|
|
|
|
| 298 |
history: Previous messages [{role, agent_id, content}, ...].
|
| 299 |
mentions: Agent IDs explicitly mentioned by user. ``None`` = auto-route.
|
| 300 |
max_history: Max history messages to include in context.
|
| 301 |
+
design_state: Persisted design state dict from previous turns.
|
| 302 |
|
| 303 |
Returns:
|
| 304 |
+
``{"responses": [...], "preview": None | {...}, "design_state": {...}}``
|
| 305 |
"""
|
| 306 |
+
state = DesignState(**(design_state or {}))
|
| 307 |
+
|
| 308 |
# Determine which agents are active
|
| 309 |
active_agents = mentions if mentions else None # None lets orchestrator decide
|
| 310 |
|
|
|
|
| 325 |
history=history,
|
| 326 |
system_prompt=system_prompt,
|
| 327 |
max_history=max_history,
|
| 328 |
+
design_state_text=state.render(),
|
| 329 |
)
|
| 330 |
|
| 331 |
# Single LLM call
|
|
|
|
| 361 |
resp["code"], message, self.output_dir, backend=self.backend,
|
| 362 |
)
|
| 363 |
|
| 364 |
+
# Update design state from responses
|
| 365 |
+
updated_state = extract_decisions(formatted, state, message)
|
| 366 |
+
|
| 367 |
+
return {"responses": formatted, "preview": preview, "design_state": updated_state.model_dump()}
|
| 368 |
|
| 369 |
|
| 370 |
# ---------------------------------------------------------------------------
|
agents/prompts.py
CHANGED
|
@@ -99,6 +99,7 @@ def build_chat_messages(
|
|
| 99 |
history: list[dict],
|
| 100 |
system_prompt: str,
|
| 101 |
max_history: int = 30,
|
|
|
|
| 102 |
) -> list[dict]:
|
| 103 |
"""Build the message list for the orchestrator LLM call.
|
| 104 |
|
|
@@ -107,6 +108,7 @@ def build_chat_messages(
|
|
| 107 |
history: Previous messages [{role, agent_id, content}, ...].
|
| 108 |
system_prompt: The orchestrator system prompt.
|
| 109 |
max_history: Maximum number of history messages to include.
|
|
|
|
| 110 |
"""
|
| 111 |
messages = [{"role": "system", "content": system_prompt}]
|
| 112 |
|
|
@@ -115,6 +117,9 @@ def build_chat_messages(
|
|
| 115 |
|
| 116 |
# Bundle history into a single context block to avoid Gemini
|
| 117 |
# treating prior agent messages as its own output and repeating them.
|
|
|
|
|
|
|
|
|
|
| 118 |
if recent:
|
| 119 |
history_lines = []
|
| 120 |
for msg in recent:
|
|
@@ -126,14 +131,10 @@ def build_chat_messages(
|
|
| 126 |
history_lines.append(f"{agent_name.upper()}: {msg['content']}")
|
| 127 |
|
| 128 |
history_block = "\n".join(history_lines)
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
"Respond to the user's NEW message above. Do NOT repeat prior responses."
|
| 134 |
-
})
|
| 135 |
-
else:
|
| 136 |
-
messages.append({"role": "user", "content": user_message})
|
| 137 |
|
| 138 |
return messages
|
| 139 |
|
|
|
|
| 99 |
history: list[dict],
|
| 100 |
system_prompt: str,
|
| 101 |
max_history: int = 30,
|
| 102 |
+
design_state_text: str = "",
|
| 103 |
) -> list[dict]:
|
| 104 |
"""Build the message list for the orchestrator LLM call.
|
| 105 |
|
|
|
|
| 108 |
history: Previous messages [{role, agent_id, content}, ...].
|
| 109 |
system_prompt: The orchestrator system prompt.
|
| 110 |
max_history: Maximum number of history messages to include.
|
| 111 |
+
design_state_text: Rendered design state spec to inject as context.
|
| 112 |
"""
|
| 113 |
messages = [{"role": "system", "content": system_prompt}]
|
| 114 |
|
|
|
|
| 117 |
|
| 118 |
# Bundle history into a single context block to avoid Gemini
|
| 119 |
# treating prior agent messages as its own output and repeating them.
|
| 120 |
+
content_parts = []
|
| 121 |
+
if design_state_text:
|
| 122 |
+
content_parts.append(f"## Current Design Spec (agreed so far)\n{design_state_text}\n")
|
| 123 |
if recent:
|
| 124 |
history_lines = []
|
| 125 |
for msg in recent:
|
|
|
|
| 131 |
history_lines.append(f"{agent_name.upper()}: {msg['content']}")
|
| 132 |
|
| 133 |
history_block = "\n".join(history_lines)
|
| 134 |
+
content_parts.append(f"## Conversation so far:\n{history_block}\n")
|
| 135 |
+
content_parts.append(f"## User's new message:\n{user_message}\n\nRespond to the user's NEW message above. Do NOT repeat prior responses.")
|
| 136 |
+
|
| 137 |
+
messages.append({"role": "user", "content": "\n".join(content_parts)})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
return messages
|
| 140 |
|
server/routes.py
CHANGED
|
@@ -33,6 +33,7 @@ class ChatRequest(BaseModel):
|
|
| 33 |
history: list[ChatMessage] = Field(default_factory=list)
|
| 34 |
mentions: list[str] = Field(default_factory=list)
|
| 35 |
backend: str = "mock"
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
class ReportRequest(BaseModel):
|
|
@@ -74,6 +75,7 @@ async def chat(body: ChatRequest):
|
|
| 74 |
message=message,
|
| 75 |
history=history,
|
| 76 |
mentions=mentions,
|
|
|
|
| 77 |
)
|
| 78 |
return JSONResponse(result)
|
| 79 |
except Exception as e:
|
|
|
|
| 33 |
history: list[ChatMessage] = Field(default_factory=list)
|
| 34 |
mentions: list[str] = Field(default_factory=list)
|
| 35 |
backend: str = "mock"
|
| 36 |
+
design_state: dict = Field(default_factory=dict)
|
| 37 |
|
| 38 |
|
| 39 |
class ReportRequest(BaseModel):
|
|
|
|
| 75 |
message=message,
|
| 76 |
history=history,
|
| 77 |
mentions=mentions,
|
| 78 |
+
design_state=body.design_state,
|
| 79 |
)
|
| 80 |
return JSONResponse(result)
|
| 81 |
except Exception as e:
|
web/index.html
CHANGED
|
@@ -1143,6 +1143,7 @@
|
|
| 1143 |
<div class="chat-header">
|
| 1144 |
<div class="chat-header-left">
|
| 1145 |
<span class="chat-header-title">Design Chat</span>
|
|
|
|
| 1146 |
<div class="agent-dots">
|
| 1147 |
<div class="agent-dot" style="background: var(--agent-design);" title="Design Agent"></div>
|
| 1148 |
<div class="agent-dot" style="background: var(--agent-engineering);" title="Engineering Agent"></div>
|
|
@@ -1244,6 +1245,7 @@
|
|
| 1244 |
|
| 1245 |
let currentBackend = 'mock';
|
| 1246 |
let chatHistory = [];
|
|
|
|
| 1247 |
let chatPanelOpen = true;
|
| 1248 |
let currentPartName = '';
|
| 1249 |
let currentCode = '';
|
|
@@ -1252,6 +1254,58 @@ const galleryItems = [];
|
|
| 1252 |
let mentionActive = false;
|
| 1253 |
let mentionIndex = 0;
|
| 1254 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1255 |
const AGENTS = {
|
| 1256 |
design: { name: 'Design', color: '#7c3aed', avatar: 'D' },
|
| 1257 |
engineering: { name: 'Engineering', color: '#00b4d8', avatar: 'E' },
|
|
@@ -1440,11 +1494,13 @@ async function sendMessage(text) {
|
|
| 1440 |
history: chatHistory,
|
| 1441 |
mentions: mentions,
|
| 1442 |
backend: currentBackend,
|
|
|
|
| 1443 |
}),
|
| 1444 |
});
|
| 1445 |
|
| 1446 |
// Add to history AFTER sending (so it's included in future turns)
|
| 1447 |
chatHistory.push({ role: 'user', content: text });
|
|
|
|
| 1448 |
const data = await resp.json();
|
| 1449 |
|
| 1450 |
hideTyping();
|
|
@@ -1463,6 +1519,11 @@ async function sendMessage(text) {
|
|
| 1463 |
chatHistory.push({ role: 'agent', agent_id: r.agent_id, content: r.message });
|
| 1464 |
}
|
| 1465 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1466 |
// If preview available, load 3D model
|
| 1467 |
if (data.preview && data.preview.success) {
|
| 1468 |
setViewerLoading(true, 'LOADING 3D MODEL...');
|
|
@@ -1907,6 +1968,16 @@ document.addEventListener('keydown', (e) => {
|
|
| 1907 |
initViewer();
|
| 1908 |
checkServer();
|
| 1909 |
setInterval(checkServer, 15000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1910 |
</script>
|
| 1911 |
</body>
|
| 1912 |
</html>
|
|
|
|
| 1143 |
<div class="chat-header">
|
| 1144 |
<div class="chat-header-left">
|
| 1145 |
<span class="chat-header-title">Design Chat</span>
|
| 1146 |
+
<button onclick="newDesign()" title="New Design" style="background:none;border:1px solid var(--border);border-radius:4px;color:var(--text-secondary);padding:2px 8px;font-size:10px;cursor:pointer;margin-left:8px;">NEW</button>
|
| 1147 |
<div class="agent-dots">
|
| 1148 |
<div class="agent-dot" style="background: var(--agent-design);" title="Design Agent"></div>
|
| 1149 |
<div class="agent-dot" style="background: var(--agent-engineering);" title="Engineering Agent"></div>
|
|
|
|
| 1245 |
|
| 1246 |
let currentBackend = 'mock';
|
| 1247 |
let chatHistory = [];
|
| 1248 |
+
let designState = {};
|
| 1249 |
let chatPanelOpen = true;
|
| 1250 |
let currentPartName = '';
|
| 1251 |
let currentCode = '';
|
|
|
|
| 1254 |
let mentionActive = false;
|
| 1255 |
let mentionIndex = 0;
|
| 1256 |
|
| 1257 |
+
// Persist/restore from localStorage
|
| 1258 |
+
function saveState() {
|
| 1259 |
+
try {
|
| 1260 |
+
localStorage.setItem('neuralcad_history', JSON.stringify(chatHistory));
|
| 1261 |
+
localStorage.setItem('neuralcad_state', JSON.stringify(designState));
|
| 1262 |
+
} catch (e) { /* quota exceeded, ignore */ }
|
| 1263 |
+
}
|
| 1264 |
+
|
| 1265 |
+
function loadState() {
|
| 1266 |
+
try {
|
| 1267 |
+
const h = localStorage.getItem('neuralcad_history');
|
| 1268 |
+
const s = localStorage.getItem('neuralcad_state');
|
| 1269 |
+
if (h) chatHistory = JSON.parse(h);
|
| 1270 |
+
if (s) designState = JSON.parse(s);
|
| 1271 |
+
} catch (e) { /* corrupted, ignore */ }
|
| 1272 |
+
}
|
| 1273 |
+
|
| 1274 |
+
function clearState() {
|
| 1275 |
+
chatHistory = [];
|
| 1276 |
+
designState = {};
|
| 1277 |
+
localStorage.removeItem('neuralcad_history');
|
| 1278 |
+
localStorage.removeItem('neuralcad_state');
|
| 1279 |
+
}
|
| 1280 |
+
|
| 1281 |
+
function newDesign() {
|
| 1282 |
+
if (!confirm('Start a new design? Current conversation will be cleared.')) return;
|
| 1283 |
+
clearState();
|
| 1284 |
+
// Clear chat UI
|
| 1285 |
+
const msgs = document.getElementById('chat-messages');
|
| 1286 |
+
if (msgs) msgs.innerHTML = '';
|
| 1287 |
+
// Show examples again
|
| 1288 |
+
const examples = document.getElementById('quick-examples');
|
| 1289 |
+
if (examples) examples.style.display = '';
|
| 1290 |
+
// Clear 3D viewer
|
| 1291 |
+
if (currentMesh) {
|
| 1292 |
+
scene.remove(currentMesh);
|
| 1293 |
+
currentMesh.geometry.dispose();
|
| 1294 |
+
currentMesh.material.dispose();
|
| 1295 |
+
currentMesh = null;
|
| 1296 |
+
}
|
| 1297 |
+
// Hide overlays
|
| 1298 |
+
const geo = document.getElementById('geo-stats');
|
| 1299 |
+
if (geo) geo.classList.remove('visible');
|
| 1300 |
+
const cnc = document.getElementById('cnc-badge');
|
| 1301 |
+
if (cnc) cnc.classList.remove('visible');
|
| 1302 |
+
const dl = document.getElementById('download-btns');
|
| 1303 |
+
if (dl) dl.classList.remove('visible');
|
| 1304 |
+
// Show empty state
|
| 1305 |
+
const empty = document.getElementById('viewer-empty');
|
| 1306 |
+
if (empty) empty.style.display = '';
|
| 1307 |
+
}
|
| 1308 |
+
|
| 1309 |
const AGENTS = {
|
| 1310 |
design: { name: 'Design', color: '#7c3aed', avatar: 'D' },
|
| 1311 |
engineering: { name: 'Engineering', color: '#00b4d8', avatar: 'E' },
|
|
|
|
| 1494 |
history: chatHistory,
|
| 1495 |
mentions: mentions,
|
| 1496 |
backend: currentBackend,
|
| 1497 |
+
design_state: designState,
|
| 1498 |
}),
|
| 1499 |
});
|
| 1500 |
|
| 1501 |
// Add to history AFTER sending (so it's included in future turns)
|
| 1502 |
chatHistory.push({ role: 'user', content: text });
|
| 1503 |
+
saveState();
|
| 1504 |
const data = await resp.json();
|
| 1505 |
|
| 1506 |
hideTyping();
|
|
|
|
| 1519 |
chatHistory.push({ role: 'agent', agent_id: r.agent_id, content: r.message });
|
| 1520 |
}
|
| 1521 |
|
| 1522 |
+
if (data.design_state) {
|
| 1523 |
+
designState = data.design_state;
|
| 1524 |
+
}
|
| 1525 |
+
saveState();
|
| 1526 |
+
|
| 1527 |
// If preview available, load 3D model
|
| 1528 |
if (data.preview && data.preview.success) {
|
| 1529 |
setViewerLoading(true, 'LOADING 3D MODEL...');
|
|
|
|
| 1968 |
initViewer();
|
| 1969 |
checkServer();
|
| 1970 |
setInterval(checkServer, 15000);
|
| 1971 |
+
|
| 1972 |
+
loadState();
|
| 1973 |
+
// Re-render restored messages
|
| 1974 |
+
if (chatHistory.length > 0) {
|
| 1975 |
+
const examples = document.getElementById('quick-examples');
|
| 1976 |
+
if (examples) examples.style.display = 'none';
|
| 1977 |
+
for (const msg of chatHistory) {
|
| 1978 |
+
addMessage(msg);
|
| 1979 |
+
}
|
| 1980 |
+
}
|
| 1981 |
</script>
|
| 1982 |
</body>
|
| 1983 |
</html>
|