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
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.2.1-alpha] - 2025-12-07

### Added

- Script tools:
- Added `script_generate_and_place_on_canvas` wrapper tool that combines `script_generate` and `gh_put` in a single call, reducing token consumption by eliminating the need for the AI to call both tools separately.
- Moved `script_generate` tool to `Hidden` category (only `script_generate_and_place_on_canvas` is now visible to chat agents).
- `gh_get` tools:
- Added `gh_get_selected_with_data` tool that returns selected components with their runtime/volatile data (actual values flowing through outputs).
- Added `gh_get_by_guid_with_data` tool that returns specific components by GUID with their runtime/volatile data.
- Added `includeRuntimeData` parameter to the base `gh_get` tool for optional runtime data extraction.
- Runtime data includes total item count, branch structure, and sample values for each parameter output.
- Added `gh_get_errors_with_data` tool that returns only errored components with their runtime/volatile data, useful for debugging broken definitions.
- `gh_put` tool:
- Added `instanceGuids` array to the tool result containing the actual GUIDs of placed components (useful for subsequent queries).

### Fixed

- Script tools:
- Fixed `script_generate_and_place_on_canvas` returning incorrect `instanceGuid`. The tool was returning the in-memory GUID from `script_generate` instead of the actual GUID assigned by Grasshopper when the component was placed on canvas. Now returns the real `instanceGuid` from `gh_put` result.
- GhJSON validation:
- Fixed `GHJsonAnalyzer.Validate` to treat missing `connections` property as an empty array instead of an error. Components without connections are now valid and won't trigger "'connections' property is missing or not an array" errors.
- Chat UI:
- Fixed critical bug where two identical user messages were collapsed into a single message in the UI. The root cause was that user messages didn't have a unique `TurnId`, causing identical messages to generate the same dedup key and replace each other. Now each user message receives a unique `TurnId` via `InteractionUtility.GenerateTurnId()`.
- Conversation session:
- Fixed TurnId inconsistency where `ToolResult` interactions were getting new TurnIds instead of inheriting from their originating `ToolCall`. The conditional check `if (string.IsNullOrWhiteSpace(toolInteraction.TurnId))` was never true because `AIBodyBuilder.EnsureTurnId()` had already assigned a new TurnId during tool execution. Changed to unconditional assignment to ensure correct turn-based metrics aggregation.

## [1.2.0-alpha] - 2025-12-06

### Added
Expand Down Expand Up @@ -56,6 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed the monolithic `script_generator` AI tool in favor of smaller, focused tools that operate purely on GhJSON.
- Updated `AIScriptGeneratorComponent` and `AIScriptReviewComponent` to support processing multiple inputs in parallel.
- Renamed `script_fix` tool to `script_review` to better reflect its review-focused behavior.
- `script_generate` no longer includes a pre-placement `instanceGuid` in its tool result; instance GUIDs are only exposed via `script_generate_and_place_on_canvas` / `gh_put` using the real canvas instance GUIDs.
- `GhJsonDeserializer`:
- Changed deserialization logic to default the UsingStandardOutputParam property to true when ShowStandardOutput is not present in the GhJSON ComponentState.
- Providers:
Expand Down
15 changes: 13 additions & 2 deletions DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ AI Tools are the interface between AI and Grasshopper, allowing to, for example,
| `list_filter` | Filters a list based on natural language criteria | ⚪ | 🟡 | 🟠 | 🟢 |
| `list_generate` | Generates a list based on a natural language prompt | ⚪ | 🟡 | 🟠 | 🟢 |
| `script_review` | Review a script for potential issues using AI-powered checks | ⚪ | 🟡 | 🟠 | 🟢 |
| `script_generate` | Create Grasshopper script components based on instructions | ⚪ | 🟡 | 🟠 | 🟢 |
| `script_edit` | Edit Grasshopper script components based on instructions and GUID | ⚪ | 🟡 | 🟠 | 🟢 |
| `script_generate` | Create Grasshopper script components based on instructions (Hidden to chat - available only in AIScriptGenerator component) | ⚪ | 🟡 | 🟠 | 🟢 |
| `script_generate_and_place_on_canvas` | Generate a new script component and place it on canvas in one call | ⚪ | 🟡 | 🟠 | 🟢 |
| `script_edit` | Edit Grasshopper script components based on instructions (Hidden to chat - available only in AIScriptGenerator component) | ⚪ | 🟡 | 🟠 | 🟢 |
| `script_edit_and_replace_on_canvas` | Edit a script component by GUID and replace it on canvas in one call | ⚪ | 🟡 | 🟠 | 🟢 |
| `script_parameter_add_input` | Add a new input parameter to a script component | ⚪ | 🟡 | - | - |
| `script_parameter_add_output` | Add a new output parameter to a script component | ⚪ | 🟡 | - | - |
| `script_parameter_remove_input` | Remove an input parameter from a script component | ⚪ | 🟡 | - | - |
Expand All @@ -80,6 +82,15 @@ AI Tools are the interface between AI and Grasshopper, allowing to, for example,
| `gh_list_categories` | List available Grasshopper categories | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_list_components` | List Grasshopper components (optionally filtered by category) | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_get` | Retrieve Grasshopper components as GhJSON with optional filters | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_get_selected` | Retrieve only the selected components from the Grasshopper canvas as GhJSON | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_get_selected_with_data` | Retrieve selected components as GhJSON together with a snapshot of their runtime data (inputs/outputs, counts, sample values) | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_get_by_guid` | Retrieve specific components by GUID as GhJSON | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_get_by_guid_with_data` | Retrieve specific components by GUID as GhJSON together with a snapshot of their runtime data | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_get_errors` | Retrieve only components that have error messages as GhJSON | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_get_errors_with_data` | Retrieve only errored components as GhJSON together with a snapshot of their runtime data (useful for debugging broken parts of a definition) | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_get_locked` | Retrieve only locked (disabled) components as GhJSON | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_get_hidden` | Retrieve only components with preview turned off (hidden geometry) as GhJSON | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_get_visible` | Retrieve only components with preview turned on (visible geometry) as GhJSON | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_put` | Place Grasshopper components on the canvas from GhJSON format | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_merge` | Merge two GhJSON documents into one (target takes priority on conflicts) | ⚪ | 🟡 | 🟠 | 🟢 |
| `gh_component_toggle_preview` | Toggle component preview on or off by GUID | ⚪ | 🟡 | 🟠 | 🟢 |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SmartHopper - AI-Powered Tools and Assistant for Grasshopper3D

[![Version](https://img.shields.io/badge/version-1.2.0--alpha-orange?style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Version](https://img.shields.io/badge/version-1.2.1--alpha-orange?style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Status](https://img.shields.io/badge/status-Alpha-orange?style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/releases)
[![.NET CI](https://img.shields.io/github/actions/workflow/status/architects-toolkit/SmartHopper/.github/workflows/ci-dotnet-tests.yml?label=tests&logo=dotnet&style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/actions/workflows/ci-dotnet-tests.yml)
[![Ready to use](https://img.shields.io/badge/ready_to_use-YES-brightgreen?style=for-the-badge)](https://smarthopper.xyz/#installation)
Expand Down
2 changes: 1 addition & 1 deletion Solution.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<SolutionVersion>1.2.0-alpha</SolutionVersion>
<SolutionVersion>1.2.1-alpha</SolutionVersion>
</PropertyGroup>
</Project>
93 changes: 85 additions & 8 deletions src/SmartHopper.Core.Grasshopper/AITools/gh_get.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,15 @@ public IEnumerable<AITool> GetTools()
""type"": ""boolean"",
""default"": false,
""description"": ""Whether to include document metadata (schema version, timestamps, Rhino/Grasshopper versions, plugin dependencies). Default is false.""
},
""includeRuntimeData"": {
""type"": ""boolean"",
""default"": false,
""description"": ""Whether to include runtime/volatile data (actual values currently flowing through component outputs). Useful for inspecting computed results. Default is false. This is token-expansive!""
}
}
}",
execute: (toolCall) => this.GhGetToolAsync(toolCall, null, null));
execute: (toolCall) => this.GhGetToolAsync(toolCall, null, null, false));

// Specialized wrapper: gh_get_selected
yield return new AITool(
Expand All @@ -102,7 +107,24 @@ public IEnumerable<AITool> GetTools()
}
}
}",
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+selected" }));
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+selected" }, null, false));

// Specialized wrapper: gh_get_selected_with_data
yield return new AITool(
name: "gh_get_selected_with_data",
description: "Read selected components WITH their runtime data (volatile data - actual values flowing through outputs). Use this when you need to inspect computed results, count items, or check actual output values. Returns GhJSON with an additional 'runtimeData' object. This is token-expansive!",
category: "Components",
parametersSchema: @"{
""type"": ""object"",
""properties"": {
""connectionDepth"": {
""type"": ""integer"",
""default"": 0,
""description"": ""Depth of connections to include: 0 (default) only selected components; 1 includes directly connected components, etc.""
}
}
}",
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+selected" }, null, true));

// Specialized wrapper: gh_get_by_guid
yield return new AITool(
Expand All @@ -125,7 +147,30 @@ public IEnumerable<AITool> GetTools()
},
""required"": [""guidFilter""]
}",
execute: (toolCall) => this.GhGetToolAsync(toolCall, null, null));
execute: (toolCall) => this.GhGetToolAsync(toolCall, null, null, false));

// Specialized wrapper: gh_get_by_guid_with_data
yield return new AITool(
name: "gh_get_by_guid_with_data",
description: "Read specific components by GUID WITH their runtime data (volatile data - actual values flowing through outputs). Use this when you need to inspect computed results from known components. Returns GhJSON with an additional 'runtimeData' object. This is token-expansive!",
category: "Components",
parametersSchema: @"{
""type"": ""object"",
""properties"": {
""guidFilter"": {
""type"": ""array"",
""items"": { ""type"": ""string"" },
""description"": ""Required list of component GUIDs to retrieve.""
},
""connectionDepth"": {
""type"": ""integer"",
""default"": 0,
""description"": ""Depth of connections to include: 0 (default) only specified components; 1 includes directly connected components, etc.""
}
},
""required"": [""guidFilter""]
}",
execute: (toolCall) => this.GhGetToolAsync(toolCall, null, null, true));

// Specialized wrapper: gh_get_errors
yield return new AITool(
Expand All @@ -142,7 +187,24 @@ public IEnumerable<AITool> GetTools()
}
}
}",
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+error" }));
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+error" }, null, false));

// Specialized wrapper: gh_get_errors_with_data
yield return new AITool(
name: "gh_get_errors_with_data",
description: "Read only components that have error messages WITH their runtime data (volatile data - actual values flowing through outputs). Use this when debugging broken components and you also need to inspect their computed results. Returns GhJSON plus a 'runtimeData' object. This is token-expansive!",
category: "Components",
parametersSchema: @"{
""type"": ""object"",
""properties"": {
""connectionDepth"": {
""type"": ""integer"",
""default"": 0,
""description"": ""Depth of connections to include: 0 (default) only error components; 1 includes directly connected components; 2 includes two-level connected components, etc.""
}
}
}",
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+error" }, null, true));

// Specialized wrapper: gh_get_locked
yield return new AITool(
Expand All @@ -159,7 +221,7 @@ public IEnumerable<AITool> GetTools()
}
}
}",
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+disabled" }));
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+disabled" }, null, false));

// Specialized wrapper: gh_get_hidden
yield return new AITool(
Expand All @@ -176,7 +238,7 @@ public IEnumerable<AITool> GetTools()
}
}
}",
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+previewoff" }));
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+previewoff" }, null, false));

// Specialized wrapper: gh_get_visible
yield return new AITool(
Expand All @@ -193,7 +255,7 @@ public IEnumerable<AITool> GetTools()
}
}
}",
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+previewon" }));
execute: (toolCall) => this.GhGetToolAsync(toolCall, new[] { "+previewon" }, null, false));
}

/// <summary>
Expand All @@ -202,8 +264,9 @@ public IEnumerable<AITool> GetTools()
/// <param name="toolCall">The tool call containing parameters.</param>
/// <param name="predefinedAttrFilters">Predefined attribute filters to apply (used by wrapper tools).</param>
/// <param name="predefinedTypeFilters">Predefined type filters to apply (used by wrapper tools).</param>
/// <param name="forceIncludeRuntimeData">When true, forces inclusion of runtime data regardless of parameter value.</param>
/// <returns>Task that returns the result of the operation.</returns>
private Task<AIReturn> GhGetToolAsync(AIToolCall toolCall, string[] predefinedAttrFilters = null, string[] predefinedTypeFilters = null)
private Task<AIReturn> GhGetToolAsync(AIToolCall toolCall, string[] predefinedAttrFilters = null, string[] predefinedTypeFilters = null, bool forceIncludeRuntimeData = false)
{
// Prepare the output
var output = new AIReturn()
Expand Down Expand Up @@ -242,6 +305,7 @@ private Task<AIReturn> GhGetToolAsync(AIToolCall toolCall, string[] predefinedAt

var connectionDepth = args["connectionDepth"]?.ToObject<int>() ?? 0;
var includeMetadata = args["includeMetadata"]?.ToObject<bool>() ?? false;
var includeRuntimeData = forceIncludeRuntimeData || (args["includeRuntimeData"]?.ToObject<bool>() ?? false);
var (includeTypes, excludeTypes) = ComponentRetriever.ParseIncludeExclude(typeFilters, ComponentRetriever.TypeSynonyms);
var (includeTags, excludeTags) = ComponentRetriever.ParseIncludeExclude(attrFilters, ComponentRetriever.FilterSynonyms);
var (includeCats, excludeCats) = ComponentRetriever.ParseIncludeExclude(categoryFilters, ComponentRetriever.CategorySynonyms);
Expand Down Expand Up @@ -614,6 +678,14 @@ private Task<AIReturn> GhGetToolAsync(AIToolCall toolCall, string[] predefinedAt
// Serialize document
var json = JsonConvert.SerializeObject(document, Formatting.None);

// Extract runtime data if requested
JObject runtimeData = null;
if (includeRuntimeData)
{
runtimeData = GhJsonSerializer.ExtractRuntimeData(resultObjects);
Debug.WriteLine($"[gh_get] Extracted runtime data for {runtimeData?.Count ?? 0} components");
}

// Package result with classifications
var toolResult = new JObject
{
Expand All @@ -622,6 +694,11 @@ private Task<AIReturn> GhGetToolAsync(AIToolCall toolCall, string[] predefinedAt
["ghjson"] = json,
};

if (runtimeData != null)
{
toolResult["runtimeData"] = runtimeData;
}

var body = AIBodyBuilder.Create()
.AddToolResult(toolResult)
.Build();
Expand Down
6 changes: 6 additions & 0 deletions src/SmartHopper.Core.Grasshopper/AITools/gh_put.cs
Original file line number Diff line number Diff line change
Expand Up @@ -469,9 +469,15 @@ IGH_DocumentObject FindOwner(IGH_Param param)

Debug.WriteLine("[gh_put] Placement complete");

// Collect actual instanceGuids of placed components
var placedGuids = result.Components
.Select(c => c.InstanceGuid.ToString())
.ToList();

var toolResult = new JObject
{
["components"] = JArray.FromObject(placed),
["instanceGuids"] = JArray.FromObject(placedGuids),
["analysis"] = analysisMsg,
};

Expand Down
Loading
Loading