Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Renamed the 'Branches Input' and 'Processed Branches' parameters to 'Data Count' and 'Iterations Count' in DeconstructMetricsComponents. Improved descriptions for both parameters.

### Fixed

- Fixed a bug in DataProcessor where results were being duplicated when multiple branches were grouped together to unsuccessfully prevent unnecessary API calls [#32](https://github.com/architects-toolkit/SmartHopper/issues/32)

## [0.2.0-alpha] - 2025-04-06

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SmartHopper - AI-Powered Grasshopper3D Plugin

[![Version](https://img.shields.io/badge/version-0%2E2%2E0--alpha-orange)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Status](https://img.shields.io/badge/status-Alpha-orange)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Version](https://img.shields.io/badge/version-0%2E2%2E1--dev%2E250418-brown)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Status](https://img.shields.io/badge/status-Unstable%20Development-brown)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Grasshopper](https://img.shields.io/badge/plugin_for-Grasshopper3D-darkgreen?logo=rhinoceros)](https://www.rhino3d.com/)
[![MistralAI](https://img.shields.io/badge/AI--powered-MistralAI-orange)](https://mistral.ai/)
[![OpenAI](https://img.shields.io/badge/AI--powered-OpenAI-blue?logo=openai)](https://openai.com/)
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>0.2.1-dev.250417</SolutionVersion>
<SolutionVersion>0.2.1-dev.250418</SolutionVersion>
</PropertyGroup>
</Project>
12 changes: 6 additions & 6 deletions src/SmartHopper.Components/List/AIListEvaluate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ public override async Task DoWorkAsync(CancellationToken token)

_result = await DataTreeProcessor.RunFunctionAsync<GH_String, GH_Boolean>(
_inputTree,
async branches =>
async (branches, reuseCount) =>
{
Debug.WriteLine($"[Worker] ProcessData called with {branches.Count} branches");
return await ProcessData(branches, _parent);
Debug.WriteLine($"[Worker] ProcessData called with {branches.Count} branches, reuse count: {reuseCount}");
return await ProcessData(branches, _parent, reuseCount);
},
onlyMatchingPaths: false,
groupIdenticalBranches: true,
Expand All @@ -122,7 +122,7 @@ public override async Task DoWorkAsync(CancellationToken token)
}
}

private static async Task<Dictionary<string, List<GH_Boolean>>> ProcessData(Dictionary<string, List<GH_String>> branches, AIListEvaluate parent)
private static async Task<Dictionary<string, List<GH_Boolean>>> ProcessData(Dictionary<string, List<GH_String>> branches, AIListEvaluate parent, int reuseCount = 1)
{
/*
* Inputs will be available as a dictionary
Expand All @@ -133,7 +133,7 @@ private static async Task<Dictionary<string, List<GH_Boolean>>> ProcessData(Dict
* the output values.
*/

Debug.WriteLine($"[Worker] Processing {branches.Count} trees");
Debug.WriteLine($"[Worker] Processing {branches.Count} trees with reuse count: {reuseCount}");
Debug.WriteLine($"[Worker] Items per tree: {branches.Values.Max(branch => branch.Count)}");

// Get the trees
Expand Down Expand Up @@ -169,7 +169,7 @@ private static async Task<Dictionary<string, List<GH_Boolean>>> ProcessData(Dict
var evaluationResult = await ListTools.EvaluateListAsync(
currentList.Value,
question,
messages => parent.GetResponse(messages, contextProviderFilter: "-environment,-time"));
messages => parent.GetResponse(messages, contextProviderFilter: "-environment,-time", reuseCount: reuseCount));

if (!evaluationResult.Success)
{
Expand Down
12 changes: 6 additions & 6 deletions src/SmartHopper.Components/List/AIListFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ public override async Task DoWorkAsync(CancellationToken token)

_result = await DataTreeProcessor.RunFunctionAsync<GH_String, GH_String>(
_inputTree,
async branches =>
async (branches, reuseCount) =>
{
Debug.WriteLine($"[Worker] ProcessData called with {branches.Count} branches");
return await ProcessData(branches, _parent);
Debug.WriteLine($"[Worker] ProcessData called with {branches.Count} branches, reuse count: {reuseCount}");
return await ProcessData(branches, _parent, reuseCount);
},
onlyMatchingPaths: false,
groupIdenticalBranches: true,
Expand All @@ -123,7 +123,7 @@ public override async Task DoWorkAsync(CancellationToken token)
}
}

private static async Task<Dictionary<string, List<GH_String>>> ProcessData(Dictionary<string, List<GH_String>> branches, AIListFilter parent)
private static async Task<Dictionary<string, List<GH_String>>> ProcessData(Dictionary<string, List<GH_String>> branches, AIListFilter parent, int reuseCount = 1)
{
/*
* Inputs will be available as a dictionary
Expand All @@ -134,7 +134,7 @@ private static async Task<Dictionary<string, List<GH_String>>> ProcessData(Dicti
* the output values.
*/

Debug.WriteLine($"[Worker] Processing {branches.Count} trees");
Debug.WriteLine($"[Worker] Processing {branches.Count} trees with reuse count: {reuseCount}");
Debug.WriteLine($"[Worker] Items per tree: {branches.Values.Max(branch => branch.Count)}");

// Get the trees
Expand Down Expand Up @@ -168,7 +168,7 @@ private static async Task<Dictionary<string, List<GH_String>>> ProcessData(Dicti
var filterResult = await ListTools.FilterListAsync(
listTreeOriginal,
criterion,
messages => parent.GetResponse(messages, contextProviderFilter: "-environment,-time"));
messages => parent.GetResponse(messages, contextProviderFilter: "-environment,-time", reuseCount: reuseCount));

if (!filterResult.Success)
{
Expand Down
12 changes: 6 additions & 6 deletions src/SmartHopper.Components/Misc/DeconstructMetricsComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ protected override void RegisterOutputParams(GH_OutputParamManager pManager)
pManager.AddIntegerParameter("Output Tokens", "O", "Number of output tokens", GH_ParamAccess.item);
pManager.AddTextParameter("Finish Reason", "F", "Reason for finishing", GH_ParamAccess.item);
pManager.AddNumberParameter("Completion Time", "T", "Time taken for completion, in seconds", GH_ParamAccess.item);
pManager.AddIntegerParameter("Input Branches", "BI", "Number of input branches", GH_ParamAccess.item);
pManager.AddIntegerParameter("Processed Branches", "BP", "Number of processed branches. This value can differ from input branches when the component detects that there are identical branch combinations.", GH_ParamAccess.item);
pManager.AddIntegerParameter("Data Count", "DC", "The number of data items that were processed by the component. This may not match the total number of items in your input lists. If the component is configured to process data in batches, this value indicates how many batches (or groups) of results the component needs to process.", GH_ParamAccess.item);
pManager.AddIntegerParameter("Iterations Count", "IC", "The number of times the component ran its calculation. If the component was set to recognize and group identical combinations of input items, it only processed each unique combination once and applied the results to all matching outputs. As a result, the iteration count may be less than the total data count.", GH_ParamAccess.item);

}

Expand All @@ -58,8 +58,8 @@ protected override void SolveInstance(IGH_DataAccess DA)
int outputTokens = metricsObject["tokens_output"]?.Value<int>() ?? 0;
string finishReason = metricsObject["finish_reason"]?.Value<string>() ?? "Unknown";
double completionTime = metricsObject["completion_time"]?.Value<double>() ?? 0.0;
int branchesInput = metricsObject["branches_input"]?.Value<int>() ?? 0;
int branchesProcessed = metricsObject["branches_processed"]?.Value<int>() ?? 0;
int inputDataCount = metricsObject["data_count"]?.Value<int>() ?? 0;
int iterationsCount = metricsObject["iterations_count"]?.Value<int>() ?? 0;


// Checks to see if the values were actually present
Expand Down Expand Up @@ -89,9 +89,9 @@ protected override void SolveInstance(IGH_DataAccess DA)
DA.SetData(5, completionTime);
if (!hasCompletionTime) AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Completion time not found in JSON");

DA.SetData(6, branchesInput);
DA.SetData(6, inputDataCount);

DA.SetData(7, branchesProcessed);
DA.SetData(7, iterationsCount);
}
catch (Exception ex)
{
Expand Down
14 changes: 7 additions & 7 deletions src/SmartHopper.Components/Text/AITextEvaluate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ public override async Task DoWorkAsync(CancellationToken token)

_result = await DataTreeProcessor.RunFunctionAsync<GH_String, GH_Boolean>(
_inputTree,
async branches =>
async (branches, reuseCount) =>
{
Debug.WriteLine($"[Worker] ProcessData called with {branches.Count} branches");
return await ProcessData(branches, _parent);
Debug.WriteLine($"[Worker] ProcessData called with {branches.Count} branches, reuse count: {reuseCount}");
return await ProcessData(branches, _parent, reuseCount);
},
onlyMatchingPaths: false,
groupIdenticalBranches: true,
Expand All @@ -120,7 +120,7 @@ public override async Task DoWorkAsync(CancellationToken token)
}
}

private static async Task<Dictionary<string, List<GH_Boolean>>> ProcessData(Dictionary<string, List<GH_String>> branches, AITextEvaluate parent)
private static async Task<Dictionary<string, List<GH_Boolean>>> ProcessData(Dictionary<string, List<GH_String>> branches, AITextEvaluate parent, int reuseCount = 1)
{
/*
* Inputs will be available as a dictionary
Expand All @@ -131,7 +131,7 @@ private static async Task<Dictionary<string, List<GH_Boolean>>> ProcessData(Dict
* the output values.
*/

Debug.WriteLine($"[Worker] Processing {branches.Count} trees");
Debug.WriteLine($"[Worker] Processing {branches.Count} trees with reuse count: {reuseCount}");
Debug.WriteLine($"[Worker] Items per tree: {branches.Values.Max(branch => branch.Count)}");

// Get the trees
Expand All @@ -158,11 +158,11 @@ private static async Task<Dictionary<string, List<GH_Boolean>>> ProcessData(Dict
{
Debug.WriteLine($"[ProcessData] Processing text {i + 1}/{textTree.Count}");

// Evaluate text using the generic tool
// Evaluate text using AI with the component's GetResponse, passing the reuseCount
var result = await TextTools.EvaluateTextAsync(
textTree[i],
questionTree[i],
messages => parent.GetResponse(messages, contextProviderFilter: "-environment,-time"));
messages => parent.GetResponse(messages, contextProviderFilter: "-environment,-time", reuseCount: reuseCount));

if (!result.Success)
{
Expand Down
12 changes: 6 additions & 6 deletions src/SmartHopper.Components/Text/AITextGenerate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ public override async Task DoWorkAsync(CancellationToken token)

_result = await DataTreeProcessor.RunFunctionAsync<GH_String, GH_String>(
_inputTree,
async branches =>
async (branches, reuseCount) =>
{
Debug.WriteLine($"[Worker] ProcessData called with {branches.Count} branches");
return await ProcessData(branches, _parent);
Debug.WriteLine($"[Worker] ProcessData called with {branches.Count} branches, reuse count: {reuseCount}");
return await ProcessData(branches, _parent, reuseCount);
},
onlyMatchingPaths: false,
groupIdenticalBranches: true,
Expand All @@ -119,7 +119,7 @@ public override async Task DoWorkAsync(CancellationToken token)
}
}

private static async Task<Dictionary<string, List<GH_String>>> ProcessData(Dictionary<string, List<GH_String>> branches, AITextGenerate parent)
private static async Task<Dictionary<string, List<GH_String>>> ProcessData(Dictionary<string, List<GH_String>> branches, AITextGenerate parent, int reuseCount = 1)
{
/*
* Inputs will be available as a dictionary
Expand All @@ -130,7 +130,7 @@ private static async Task<Dictionary<string, List<GH_String>>> ProcessData(Dicti
* the output values.
*/

Debug.WriteLine($"[Worker] Processing {branches.Count} trees");
Debug.WriteLine($"[Worker] Processing {branches.Count} trees with reuse count: {reuseCount}");
Debug.WriteLine($"[Worker] Items per tree: {branches.Values.Max(branch => branch.Count)}");

// Get the trees
Expand Down Expand Up @@ -161,7 +161,7 @@ private static async Task<Dictionary<string, List<GH_String>>> ProcessData(Dicti
var result = await TextTools.GenerateTextAsync(
promptTree[i],
instructionsTree[i],
messages => parent.GetResponse(messages, contextProviderFilter: "-environment,-time"));
messages => parent.GetResponse(messages, contextProviderFilter: "-environment,-time", reuseCount: reuseCount));

if (!result.Success)
{
Expand Down
6 changes: 6 additions & 0 deletions src/SmartHopper.Config/Models/AIResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@ public class AIResponse
public string ToolArguments { get; set; }
public string Provider { get; set; }
public string Model { get; set; }

/// <summary>
/// Tracks how many times this response is reused across different data tree branches.
/// Default is 1 (used once).
/// </summary>
public int ReuseCount { get; set; } = 1;
}
}
2 changes: 1 addition & 1 deletion src/SmartHopper.Core.Grasshopper/Tools/TextTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public static async Task<AIEvaluationResult<GH_Boolean>> EvaluateTextAsync(
// User message
new KeyValuePair<string, string>("user",
$"This is my question: \"{question.Value}\"\n\n" +
$"Answer to the previous question on the following text:\n{text.Value}\n\n")
$"Answer the previous question based on the following input:\n{text.Value}\n\n")
};

// Get response using the provided function
Expand Down
24 changes: 14 additions & 10 deletions src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,13 @@ protected override List<string> InputsChanged()
/// <param name="messages">The messages to send to the AI provider.</param>
/// <param name="contextProviderFilter">Optional filter for context providers (comma-separated list).</param>
/// <param name="contextKeyFilter">Optional filter for context keys (comma-separated list).</param>
/// <param name="reuseCount">Optional. The number of times this response will be reused across different branches. Default is 1.</param>
/// <returns>The AI response from the provider.</returns>
protected async Task<AIResponse> GetResponse(
List<KeyValuePair<string, string>> messages,
string contextProviderFilter = null,
string contextKeyFilter = null)
string contextKeyFilter = null,
int reuseCount = 1)
{
try
{
Expand All @@ -274,7 +276,8 @@ protected async Task<AIResponse> GetResponse(
contextProviderFilter: contextProviderFilter,
contextKeyFilter: contextKeyFilter);

StoreResponseMetrics(response);
// Store response metrics with the provided reuse count
StoreResponseMetrics(response, reuseCount);

return response;
}
Expand Down Expand Up @@ -327,22 +330,25 @@ protected void AIErrorToPersistentRuntimeMessage(AIResponse response)
/// Stores the given AI response metrics in the component's internal metrics list.
/// </summary>
/// <param name="response">The AI response to store metrics from.</param>
public void StoreResponseMetrics(AIResponse response)
/// <param name="reuseCount">Optional. The number of times this response is reused across different branches. Default is 1.</param>
public void StoreResponseMetrics(AIResponse response, int reuseCount = 1)
{
if (response != null)
{
// Set the reuse count on the response
response.ReuseCount = reuseCount;

_responseMetrics.Add(response);

Debug.WriteLine("[AIStatefulAsyncComponentBase] [StoreResponseMetrics] Added response to metrics list");
Debug.WriteLine($"[AIStatefulAsyncComponentBase] [StoreResponseMetrics] Added response to metrics list with reuse count: {reuseCount}");
}
}

/// <summary>
/// Sets the metrics output parameters (input tokens, output tokens, finish reason)
/// </summary>
/// <param name="DA">The data access object</param>
/// <param name="initialBranches">The number of branches in the input data structure</param>
protected void SetMetricsOutput(IGH_DataAccess DA, int initialBranches = 0)
protected void SetMetricsOutput(IGH_DataAccess DA)
{
Debug.WriteLine("[AIStatefulComponentBase] SetMetricsOutput - Start");

Expand Down Expand Up @@ -372,12 +378,10 @@ protected void SetMetricsOutput(IGH_DataAccess DA, int initialBranches = 0)
new JProperty("tokens_output", totalOutTokens),
new JProperty("finish_reason", finishReason),
new JProperty("completion_time", totalCompletionTime),
new JProperty("branches_input", initialBranches),
new JProperty("branches_processed", _responseMetrics.Count)
new JProperty("data_count", _responseMetrics.Sum(r => r.ReuseCount)),
new JProperty("iterations_count", _responseMetrics.Count)
);

// DA.SetData("Metrics", metricsJson);

// Convert metricsJson to GH_String
var metricsJsonString = metricsJson.ToString();
var ghString = new GH_String(metricsJsonString);
Expand Down
Loading