Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ A sketching and diagramming app built with [Jaclang](https://docs.jaseci.org/) a
python -m venv .venv
source .venv/bin/activate
pip install jaclang jac-client jac-scale
jac start
jac start main.jac
```

## Project Structure
Expand Down
18 changes: 14 additions & 4 deletions agent/orchestrator.jac
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import from .subagents {
DiagramComposer,
DiagramEditor
}
import from .subagents.analyzer { _set_analyst_provider }
import from .subagents.synthesizer { _set_composer_provider }
import from .subagents.refiner { _set_editor_provider }
import from .llm_factory { create_llm }
Expand Down Expand Up @@ -167,6 +166,7 @@ def _send_command(action: str, data: dict | None = None, session_id: str = "", t
}

payload: str = json.dumps({
"role": "controller",
"requestId": request_id,
"sessionId": session_id,
"action": action,
Expand Down Expand Up @@ -258,7 +258,7 @@ walker AgentOrchestrator {
can init_context with Root entry {
# Find or create SessionContext for this session
self._provider = CanvasToolProvider(
send_fn=lambda (action: str, data: dict[str, Any]) -> Any {_send_command(action, data, self.session_id);}
send_fn=lambda (action: str, data: dict[str, Any]) -> Any {return _send_command(action, data, self.session_id);}
);

# Find or create session context (persists across turns)
Expand Down Expand Up @@ -431,6 +431,7 @@ walker AgentOrchestrator {
log(f"compose_diagram: response = {response[:80]}");

self._session_context.clear_diagram_blueprint();
self._session_context.canvas_summary = "non-empty";
self._session_context.append_turn("composer", response);
_send_ws(self.ws, {"type": "message", "sessionId": self.session_id, "content": response}, self.event_loop);
_send_ws(self.ws, {"type": "done", "sessionId": self.session_id}, self.event_loop);
Expand All @@ -443,11 +444,20 @@ walker AgentOrchestrator {
if _diagram_analyst is None { return; }

log("explain_canvas: starting DiagramAnalyst");
_set_analyst_provider(self._provider);

# Pre-fetch canvas elements directly (same pattern as _check_canvas_empty)
canvas_elements_json: str = '{"elements":[],"total":0}';
try {
canvas_elements_json = self._provider.call_tool("get_canvas_elements", {});
} except Exception as e {
log(f"explain_canvas: error fetching elements: {e}");
}
log(f"explain_canvas: canvas_elements_json = {canvas_elements_json[:120]}");

response: str = _diagram_analyst.explain(
self.message,
self._session_context.conversation_log
self._session_context.conversation_log,
canvas_elements_json
);
log(f"explain_canvas: response = {response[:80]}");

Expand Down
43 changes: 8 additions & 35 deletions agent/subagents/analyzer.jac
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""DiagramAnalyst — explains/describes the diagram on the canvas.

Fetches canvas elements via get_canvas_elements tool and provides analysis.
Canvas elements are pre-fetched by the orchestrator and passed as canvas_elements_json.
Read-only agent: no modification tools.
"""

Expand All @@ -12,12 +12,11 @@ node DiagramAnalyst {
has api_key: str = "";
has llm: Any = None;

def explain(message: str, conversation_log: str) -> str
def explain(message: str, conversation_log: str, canvas_elements_json: str) -> str
by self.llm(
tools=[use_canvas_tool],
max_react_iterations=2,
tools=[],
incl_info={
"available_canvas_tools": [t for t in _analyst_provider.list_tools() if t["name"] in ANALYST_TOOLS],
"canvas_elements_json": canvas_elements_json,
"conversation": conversation_log
}
);
Expand All @@ -26,10 +25,7 @@ node DiagramAnalyst {
sem DiagramAnalyst.explain = """
Explain the JaSketch canvas diagram to the user.

Workflow:
1. Call use_canvas_tool("get_canvas_elements", "{}") to fetch the current diagram
2. Analyze the elements to understand the diagram structure
3. Explain to the user what the diagram shows, its purpose, key components, and relationships
Canvas data is already provided in `canvas_elements_json` from context — parse it directly.

Element Types and Key Fields (runtime format):
- rectangle / circle / diamond: x, y, width, height, shapeText (label inside shape), color, fillColor, boundElements (list of connected arrow IDs)
Expand All @@ -41,39 +37,16 @@ Element Types and Key Fields (runtime format):
- freehand: x, y, points (free-drawn path)

Your Task:
1. Fetch canvas elements via use_canvas_tool("get_canvas_elements", "{}")
1. Parse canvas_elements_json from incl_info to extract the "elements" array
2. Identify shapes (by type + shapeText labels) and connections (arrows via startBinding/endBinding elementIds)
3. Understand the overall diagram structure: what it represents, its purpose, layout
4. Explain clearly: what diagram this is, what it shows, key components, and how they relate

If canvas_elements_json is empty or has no elements, explain that the canvas is currently empty.

Read-only: explain only, do NOT attempt to modify the canvas.

User message: {{message}}

Respond now.
""";

# ── Tool binding (populated by orchestrator before each call) ──────────────────

glob ANALYST_TOOLS: list[str] = ["get_canvas_elements"];

glob _analyst_provider: Any = None;

def use_canvas_tool(name: str, arguments: str) -> str {
args: dict = {};
try {
if isinstance(arguments, dict) { args = arguments; }
elif arguments { args = json.loads(arguments); }
} except Exception { args = {}; }
return _analyst_provider.call_tool(name, args);
}
sem use_canvas_tool = "Call a canvas tool by name. Only use tools listed in available_canvas_tools from context.";
sem use_canvas_tool.name = "Tool name — must exactly match one of the names in available_canvas_tools.";
sem use_canvas_tool.arguments = "JSON object string matching the inputSchema of the named tool. Build from the schema in available_canvas_tools.";

# ── Provider injection (called by orchestrator) ────────────────────────────────

def _set_analyst_provider(provider: Any) -> None {
global _analyst_provider;
_analyst_provider = provider;
}
2 changes: 1 addition & 1 deletion components/layout/ChatPanel.cl.jac
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def:pub ChatPanel(props: dict) -> JsxElement {
modelOptions = [
{"id": "gpt-4o", "label": "GPT-4o"},
{"id": "gpt-4o-mini", "label": "GPT-4o Mini"},
{"id": "gpt-5.4-mini", "label": "GPT-5.4 Mini"},
{"id": "gpt-5-mini", "label": "GPT-5 Mini"},
{"id": "claude-sonnet-4-20250514", "label": "Claude Sonnet"},
{"id": "claude-haiku-4-20250414", "label": "Claude Haiku"},
{"id": "gemini/gemini-2.0-flash", "label": "Gemini Flash"},
Expand Down
4 changes: 4 additions & 0 deletions hooks/useElements.cl.jac
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ def:pub useElements() -> dict {
historyRef = useRef([]);
historyIndexRef = useRef(-1);
loadedRef = useRef(False);
elementsRef = useRef([]);

STORAGE_KEY = "jasketch_elements";

def setElementsInternal(newElements: list) -> None {
elements = newElements;
elementsRef.current = newElements;
# Strip non-serializable imageObj before saving to localStorage
toStore = [
el.type == "image" and el.imageObj and {
Expand All @@ -36,6 +38,7 @@ def:pub useElements() -> dict {
mi = mi + 1;
}
elements = parsed;
elementsRef.current = parsed;
pushHistory(parsed);
}
}
Expand Down Expand Up @@ -194,6 +197,7 @@ def:pub useElements() -> dict {

return {
"elements": elements,
"elementsRef": elementsRef,
"setElements": setElementsWithHistory,
"addElement": addElement,
"addMultipleElements": addMultipleElements,
Expand Down
7 changes: 4 additions & 3 deletions hooks/useWebSocket.cl.jac
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def:pub useWebSocket(elementsHook: dict, viewportHook: dict, canvasRef: any, can
}

def sendResponse(ws: any, requestId: str, success: bool, data: any) -> None {
msg = {"requestId": requestId, "success": success, "data": data};
msg = {"role": "canvas", "requestId": requestId, "success": success, "data": data};
ws.send(JSON.stringify(msg));
}

Expand Down Expand Up @@ -70,7 +70,8 @@ def:pub useWebSocket(elementsHook: dict, viewportHook: dict, canvasRef: any, can
}
sendResponse(ws, requestId, True, {"ids": addedIds, "count": items.length, "action": "created"});
} elif action == "getElements" {
elems = hook.elements;
# Use elementsRef for synchronous read (avoids stale React state snapshot)
elems = (hook.elementsRef and hook.elementsRef.current) or hook.elements;
result = [];
for el in elems {
if el and (not data.type or el.type == data.type) {
Expand Down Expand Up @@ -208,8 +209,8 @@ def:pub useWebSocket(elementsHook: dict, viewportHook: dict, canvasRef: any, can
sendError(ws, requestId, "Element not found: " + findTargetId);
}
} elif action == "setElements" {
hook.clearElements();
newElements = data.elements or [];
hook.clearElements();
hook.addMultipleElements(newElements);

# Auto-fit viewport: compute bounding box of all elements, then zoom to fit with 60px padding
Expand Down
2 changes: 1 addition & 1 deletion mcp-server/jasketch_mcp/canvas_tool_provider.jac
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ obj CanvasToolProvider {
"""Fetch all canvas elements with defaults stripped for token efficiency."""
def _get_canvas_elements(args: dict) -> str {
result: Any = self._send("getElements", {});
if not result or not result.get("elements") {
if not result or "elements" not in result {
return json.dumps({"elements": [], "total": 0}, separators=(',', ':'));
}
# Strip {id, element} wrapper → plain element objects
Expand Down
Loading