diff --git a/README.md b/README.md index ed6404cd..af6b8147 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ You can also use local models, e.g., via [Ollama](https://ollama.com/). Download Select a cell and type `=PROMPT("What model are you and who made you?")`. For Gemma 3 4B, it will tell you that it's called "Gemma" and made by Google DeepMind. -You can also use cell references. For example, copy a news article into cell A1 and type in cell B1: `=PROMPT("Extract all person names mentioned in the text", A1)`. You can reference many cells using standard Excel notation, e.g. `=PROMPT("Extract all person names in the cells", A1:F10)` +You can also use cell references. For example, copy a news article into cell A1 and type in cell B1: `=PROMPT("Extract all person names mentioned in the text", A1)`. You can reference many cells using standard Excel notation, e.g. `=PROMPT("Extract all person names in the cells", A1:F10)` or reference multiple separate ranges, e.g. `=PROMPT("Compare these datasets", A1:B10, D1:E10)` For more advanced usage, including function calling and configuration, see our [documentation](https://docs.getcellm.com). @@ -99,7 +99,7 @@ We think Cellm is really cool because it enables everyone to automate tasks with To help us improve Cellm, we collect limited, anonymous telemetry data: - **Crash reports:** To help us fix bugs. -- **Prompts:** To help us understand usage patterns. For example, if you use `=PROMPT(A1:B2, "Extract person names")`, we capture the text "Extract person names" and prompt options. The prompt options are things like the model you use and the temperature setting. We do not capture the data in cells A1:B2. +- **Prompts:** To help us understand usage patterns. For example, if you use `=PROMPT("Extract person names", A1:B2)`, we capture the text "Extract person names" and prompt options. The prompt options are things like the model you use and the temperature setting. We do not capture the data in cells A1:B2. We do not collect any data from your spreadsheet and we have no way of associating your prompts with you. You can see for yourself at [src/Cellm/Models/Behaviors/SentryBehavior.cs](src/Cellm/Models/Behaviors/SentryBehavior.cs). diff --git a/docs/api-reference/functions/prompt-model.mdx b/docs/api-reference/functions/prompt-model.mdx index b1b52d63..9e0515d7 100644 --- a/docs/api-reference/functions/prompt-model.mdx +++ b/docs/api-reference/functions/prompt-model.mdx @@ -18,24 +18,10 @@ Allows you to call a model from a cell formula and specify the model as the firs Can be a string, a cell reference, or a range of cells. - - Either context cells or a temperature value: + + One or more cell references or ranges as context for your prompt (e.g., A1, B2:C3, D4). - - **If providing cells:** A cell reference or range (e.g., A1:D10). The model will use these cells as context when following your instructions. - - **If providing a temperature:** A number (0.0-1.0) or preset string to control randomness when no context cells are needed. - - - - Controls randomness in the model's output. - - Accepts either: - - A number between 0.0 and 1.0 - - A preset string: - - `"Consistent"` (0.0) - Deterministic output, same result each time - - `"Neutral"` (0.3) - Balanced between consistency and variety - - `"Creative"` (0.7) - More varied and creative outputs - - Lower values (closer to 0) produce consistent, deterministic responses. Higher values (closer to 1) produce more varied, creative responses. + You can provide multiple separate cell references that will all be included as context. ## Returns @@ -44,6 +30,10 @@ Allows you to call a model from a cell formula and specify the model as the firs The model's response as plain text. + + Temperature is configured via the ribbon UI. Use the Temperature dropdown in the Model section to control randomness (Consistent/0.0, Neutral/0.3, Creative/0.7, or any value 0.0-1.0). + + ```excel Text Instructions @@ -58,8 +48,12 @@ Allows you to call a model from a cell formula and specify the model as the firs =PROMPTMODEL("openai/gpt-4o-mini", "Extract keywords", A1:D10) ``` -```excel With Temperature -=PROMPTMODEL("openai/gpt-4o-mini", "Extract keywords", A1:D10, 0.7) +```excel Multiple Cell Ranges +=PROMPTMODEL("openai/gpt-4o-mini", "Compare these datasets", A1:B10, D1:E10) +``` + +```excel Mixed Cell References +=PROMPTMODEL("openai/gpt-4o-mini", "Analyze all data", A1, B2:C5, D6) ``` @@ -78,4 +72,4 @@ Allows you to call a model from a cell formula and specify the model as the firs =PROMPTMODEL.TORANGE("openai/gpt-4o-mini", "Extract keywords", A1:D10) ``` - \ No newline at end of file + diff --git a/docs/api-reference/functions/prompt.mdx b/docs/api-reference/functions/prompt.mdx index ce8226a0..aeb5d5b0 100644 --- a/docs/api-reference/functions/prompt.mdx +++ b/docs/api-reference/functions/prompt.mdx @@ -12,30 +12,20 @@ Allows you to call the default model from a cell formula. Can be a string, a cell reference, or a range of cells. - - Either context cells or a temperature value: + + One or more cell references or ranges as context for your prompt (e.g., A1, B2:C3, D4). - - **If providing cells:** A cell reference or range (e.g., A1:D10). The model will use these cells as context when following your instructions. - - **If providing a temperature:** A number (0.0-1.0) or preset string to control randomness when no context cells are needed. - - - - Controls randomness in the model's output. - - Accepts either: - - A number between 0.0 and 1.0 - - A preset string: - - `"Consistent"` (0.0) - Deterministic output, same result each time - - `"Neutral"` (0.3) - Balanced between consistency and variety - - `"Creative"` (0.7) - More varied and creative outputs - - Lower values (closer to 0) produce consistent, deterministic responses. Higher values (closer to 1) produce more varied, creative responses. + You can provide multiple separate cell references that will all be included as context. The model's response as plain text. + + Temperature is configured via the ribbon UI. Use the Temperature dropdown in the Model section to control randomness (Consistent/0.0, Neutral/0.3, Creative/0.7, or any value 0.0-1.0). + + ```excel Text Instructions @@ -50,8 +40,12 @@ Allows you to call the default model from a cell formula. =PROMPT("Extract keywords", A1:D10) ``` -```excel With Temperature -=PROMPT("Extract keywords", A1:D10, 0.7) +```excel Multiple Cell Ranges +=PROMPT("Compare these datasets", A1:B10, D1:E10) +``` + +```excel Mixed Cell References +=PROMPT("Analyze all data", A1, B2:C5, D6) ``` @@ -70,4 +64,4 @@ Allows you to call the default model from a cell formula. =PROMPT.TORANGE("Extract keywords", A1:D10) ``` - \ No newline at end of file + diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 57ed892a..65a0db06 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -110,6 +110,12 @@ You can reference a group of cells using standard Excel notation (this is called =PROMPT("Extract all person names in the cells", A1:F10) ```` +You can also reference multiple separate cell ranges: + +````mdx Multiple ranges +=PROMPT("Compare the data in these two tables", A1:C10, E1:G10) +```` + ### Spilling model response across multiple cells You can use the `=PROMPT.TOCOLUMN()` function to spill the model response across multiple cells. For example, if you have text in cell A1, you can type in cell B1: diff --git a/src/Cellm/AddIn/ArgumentParser.cs b/src/Cellm/AddIn/ArgumentParser.cs index 01c603dd..8a611178 100644 --- a/src/Cellm/AddIn/ArgumentParser.cs +++ b/src/Cellm/AddIn/ArgumentParser.cs @@ -13,8 +13,7 @@ public class ArgumentParser(IConfiguration configuration) private object? _provider; private object? _model; private object? _instructions; - private object? _cellsOrTemperature; - private object? _temperature; + private object[]? _ranges; private StructuredOutputShape _outputShape = StructuredOutputShape.None; public static readonly string CellsBeginTag = ""; @@ -43,16 +42,9 @@ public ArgumentParser AddInstructions(object instructions) return this; } - public ArgumentParser AddCellsOrTemperature(object cellsOrTemperature) + public ArgumentParser AddCells(object[] ranges) { - _cellsOrTemperature = cellsOrTemperature; - - return this; - } - - public ArgumentParser AddTemperature(object temperature) - { - _temperature = temperature; + _ranges = ranges; return this; } @@ -82,34 +74,32 @@ internal Arguments Parse() var defaultTemperature = configuration[$"{nameof(CellmAddInConfiguration)}:{nameof(CellmAddInConfiguration.DefaultTemperature)}"] ?? throw new ArgumentException(nameof(CellmAddInConfiguration.DefaultTemperature)); - var arguments = (_instructions, _cellsOrTemperature, _temperature) switch + var temperature = ParseTemperature(defaultTemperature); + + var arguments = (_instructions, _ranges) switch { // =PROMPT("Hello world") - (string instructions, ExcelMissing, ExcelMissing) => new Arguments(provider, model, string.Empty, instructions, ParseTemperature(defaultTemperature), _outputShape), - // =PROMPT("Hello world", 0.7) - (string instructions, double temperature, ExcelMissing) => new Arguments(provider, model, string.Empty, instructions, ParseTemperature(temperature), _outputShape), - // =PROMPT("Hello world", A1:B2) - (string instructions, ExcelReference cells, ExcelMissing) => new Arguments(provider, model, new Cells(cells.RowFirst, cells.ColumnFirst, cells.GetValue()), instructions, ParseTemperature(defaultTemperature), _outputShape), - // =PROMPT("Hello world", A1:B2, 0.7) - (string instructions, ExcelReference cells, double temperature) => new Arguments(provider, model, new Cells(cells.RowFirst, cells.ColumnFirst, cells.GetValue()), instructions, ParseTemperature(temperature), _outputShape), + (string instructions, []) => new Arguments(provider, model, [], instructions, temperature, _outputShape), + // =PROMPT("Hello world", A1, B2, ...) + (string instructions, object[] ranges) => new Arguments(provider, model, ParseRanges(ranges), instructions, temperature, _outputShape), // =PROMPT(A1:B2) - (ExcelReference instructions, ExcelMissing, ExcelMissing) => new Arguments(provider, model, string.Empty, new Cells(instructions.RowFirst, instructions.ColumnFirst, instructions.GetValue()), ParseTemperature(defaultTemperature), _outputShape), - // =PROMPT(A1:B2, 0.7) - (ExcelReference instructions, double temperature, ExcelMissing) => new Arguments(provider, model, string.Empty, new Cells(instructions.RowFirst, instructions.ColumnFirst, instructions.GetValue()), ParseTemperature(temperature), _outputShape), - // =PROMPT(A1:B2, C1:D2) - (ExcelReference instructions, ExcelReference cells, ExcelMissing) => new Arguments(provider, model, new Cells(cells.RowFirst, cells.ColumnFirst, cells.GetValue()), new Cells(instructions.RowFirst, instructions.ColumnFirst, instructions.GetValue()), ParseTemperature(defaultTemperature), _outputShape), - // =PROMPT(A1:B2, C1:D2, 0.7) - (ExcelReference instructions, ExcelReference cells, double temperature) => new Arguments(provider, model, new Cells(cells.RowFirst, cells.ColumnFirst, cells.GetValue()), new Cells(instructions.RowFirst, instructions.ColumnFirst, instructions.GetValue()), ParseTemperature(temperature), _outputShape), + (ExcelReference instructions, []) => new Arguments(provider, model, [], new Range(instructions.RowFirst, instructions.ColumnFirst, instructions.GetValue()), temperature, _outputShape), + // =PROMPT(A1:B2, C1, D2, ...) + (ExcelReference instructions, object[] ranges) => new Arguments(provider, model, ParseRanges(ranges), new Range(instructions.RowFirst, instructions.ColumnFirst, instructions.GetValue()), temperature, _outputShape), // Anything else - _ => throw new ArgumentException($"Invalid arguments ({_instructions?.GetType().Name}, {_cellsOrTemperature?.GetType().Name}, {_temperature?.GetType().Name})") + _ => throw new ArgumentException($"Invalid arguments ({_instructions?.GetType().Name}, {_ranges?.GetType().Name})") }; - if (arguments.Cells is Cells contextCells && contextCells.Values is ExcelError contextCellsError) + // Validate cells for errors + foreach (var range in arguments.Ranges) { - throw new ExcelErrorException(contextCellsError); + if (range.Values is ExcelError rangeError) + { + throw new ExcelErrorException(rangeError); + } } - if (arguments.Instructions is Cells instructionCells && instructionCells.Values is ExcelError instructionsCellsError) + if (arguments.Instructions is Range instructionCells && instructionCells.Values is ExcelError instructionsCellsError) { throw new ExcelErrorException(instructionsCellsError); } @@ -117,16 +107,39 @@ internal Arguments Parse() return arguments; } - internal static string AddCells(string cells) + private static List ParseRanges(object[] ranges) + { + var result = new List(); + + foreach (var range in ranges) + { + switch (range) + { + case ExcelReference excelReference: + result.Add(new Range(excelReference.RowFirst, excelReference.ColumnFirst, excelReference.GetValue())); + break; + case ExcelMissing: + break; + case ExcelError error: + throw new ExcelErrorException(error); + default: + throw new ArgumentException($"Expected cell reference, got {range?.GetType().Name}"); + } + } + + return result; + } + + internal static string FormatRanges(string ranges) { return new StringBuilder() .AppendLine(CellsBeginTag) - .AppendLine(cells) + .AppendLine(ranges) .AppendLine(CellsEndTag) .ToString(); } - internal static string AddInstructions(string instructions) + internal static string FormatInstructions(string instructions) { return new StringBuilder() .AppendLine(InstructionsBeginTag) @@ -136,9 +149,9 @@ internal static string AddInstructions(string instructions) } // Render range as Markdown table because models have seen loads of those - internal static string ParseCells(Cells cells) + internal static string RenderRange(Range range) { - var values = cells.Values switch + var values = range.Values switch { ExcelError excelError => throw new ExcelErrorException(excelError), object[,] manyCells => manyCells, @@ -161,14 +174,14 @@ internal static string ParseCells(Cells cells) // Add header row for (var c = 1; c < numberOfRenderedColumns; c++) { - table[0, c] = GetColumnName(cells.ColumnFirst + c - 1); + table[0, c] = GetColumnName(range.ColumnFirst + c - 1); maxColumnWidth[c] = table[0, c].Length; } // Add enumeration column for (var r = 0; r < numberOfRows; r++) { - table[r + 1, 0] = GetRowName(cells.RowFirst + r); + table[r + 1, 0] = GetRowName(range.RowFirst + r); } // Parse cells and track empty rows, empty columns, and max column width along the way @@ -263,14 +276,24 @@ internal static string ParseCells(Cells cells) if (rowsWithValues.Count == 0) { return $"The user has provided the cell range " + - $"{GetColumnName(cells.ColumnFirst)}{GetRowName(cells.RowFirst)}:" + - $"{GetColumnName(cells.ColumnFirst + values.GetLength(1) - 1)}{GetRowName(cells.RowFirst + values.GetLength(0) - 1)}, " + + $"{GetColumnName(range.ColumnFirst)}{GetRowName(range.RowFirst)}:" + + $"{GetColumnName(range.ColumnFirst + values.GetLength(1) - 1)}{GetRowName(range.RowFirst + values.GetLength(0) - 1)}, " + $"but all cells are empty."; } return tableBuilder.ToString(); } + internal static string RenderRanges(IReadOnlyList ranges) + { + if (ranges.Count == 0) + { + return $"The user provided no additional context."; + } + + return string.Join(Environment.NewLine, ranges.Select(RenderRange)); + } + private static double ParseTemperature(string temperatureAsString) { if (double.TryParse(temperatureAsString.Replace(',', '.'), NumberStyles.Any, CultureInfo.GetCultureInfo("en-US"), out var temperature)) diff --git a/src/Cellm/AddIn/Arguments.cs b/src/Cellm/AddIn/Arguments.cs index d101af4e..e1246244 100644 --- a/src/Cellm/AddIn/Arguments.cs +++ b/src/Cellm/AddIn/Arguments.cs @@ -3,4 +3,4 @@ namespace Cellm.AddIn; -internal record Arguments(Provider Provider, string Model, object Cells, object Instructions, double Temperature, StructuredOutputShape OutputShape); +internal record Arguments(Provider Provider, string Model, IReadOnlyList Ranges, object Instructions, double Temperature, StructuredOutputShape OutputShape); diff --git a/src/Cellm/AddIn/CellmFunctions.cs b/src/Cellm/AddIn/CellmFunctions.cs index 9bdb0132..cbaab0a8 100644 --- a/src/Cellm/AddIn/CellmFunctions.cs +++ b/src/Cellm/AddIn/CellmFunctions.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Text; using Cellm.AddIn.Exceptions; using Cellm.Models; @@ -14,8 +14,8 @@ namespace Cellm.AddIn; public static class CellmFunctions { - private const string _promptDescription = "Sends a prompt to the default model. Can be used with or without a cell range as context."; - private const string _promptModelDescription = "Sends a prompt to the specified model. Can be used with or without a cell range as context."; + private const string _promptDescription = "Sends a prompt to the default model. Can be used with or without cell ranges as context."; + private const string _promptModelDescription = "Sends a prompt to the specified model. Can be used with or without cell ranges as context."; private const string _structuredOutputShapeRowDescription = " Multiple output values spill into cells to the right."; private const string _structuredOutputShapeColumnDescription = " Multiple output values spill into cells below."; @@ -24,199 +24,170 @@ public static class CellmFunctions private const string _promptExample = $""" Example: - =PROMPT("Extract named entities", A1:B2, 0.7) + =PROMPT("Extract named entities", A1:B2, C3:D4) """; private const string _promptModelExample = """ Example: - =PROMPT("openai/gpt-4.1", "Extract named entities", A1:B2, 0.7) + =PROMPTMODEL("openai/gpt-4.1", "Extract named entities", A1:B2, C3:D4) """; private const string _instructionsName = "Prompt"; private const string _instructionsDescription = "The prompt to send to the model (string, cell, or cell range e.g., A1:B2)."; - private const string _cellsOrTemperatureName = "Cells or temperature"; - private const string _cellsOrTemperatureDescription = "(Optional) Context cells for the prompt (cell or cell range e.g., A1:B2) or the model's temperature (0.0 - 1.0) if no context is provided."; - - private const string _temperatureName = "Temperature"; - private const string _temperatureDescription = "(Optional) The model's temperature (0.0 - 1.0) when the second argument contains context cells."; + private const string _cellsName = "Cells"; + private const string _cellsDescription = "(Optional) One or more cell ranges as context (e.g., A1, B2:C3)."; private const string _providerAndModelName = "Provider and model"; private const string _promptAndModelDescription = @"The provider and model on the form ""{provider}/{model}"" (e.g., openai/gpt-4.1)"; /// -    /// Sends a prompt to the default model configured in CellmConfiguration. -    /// -    /// + /// Sends a prompt to the default model configured in CellmConfiguration. + /// + /// /// The prompt to send to the model (string, cell, or cell range). /// -    /// -    /// Context cells for the prompt (cell or cell range) or a temperature value. -    /// If cells are provided, they will be used as context for the prompt. -    /// -    /// -    /// A value between 0 and 1 that controls the randomness of the model's output. -    /// Lower values make the output more deterministic, higher values make it more random. -    /// -    /// -    /// The model's response in a single cell. If an error occurs, it returns the error message. -    /// + /// + /// Optional cell ranges to provide as context for the prompt. + /// + /// + /// The model's response in a single cell. If an error occurs, it returns the error message. + /// [ExcelFunction(Name = "PROMPT", Description = _promptDescription + _promptExample, IsThreadSafe = true, IsVolatile = false)] public static object Prompt( [ExcelArgument(AllowReference = true, Name = _instructionsName, Description = _instructionsDescription)] object instructions, - [ExcelArgument(AllowReference = true, Name = _cellsOrTemperatureName, Description = _cellsOrTemperatureDescription)] object cellsOrTemperature, - [ExcelArgument(Name = _temperatureName, Description = _temperatureDescription)] object temperature) + [ExcelArgument(AllowReference = true, Name = _cellsName, Description = _cellsDescription)] params object[] ranges) { return Run( instructions, - cellsOrTemperature, - temperature, + ranges, StructuredOutputShape.None); } -    /// -    /// Same as Prompt, but array response spill into cells to the right. -    /// -    [ExcelFunction(Name = "PROMPT.TOROW", Description = _promptDescription + _structuredOutputShapeRowDescription, IsThreadSafe = true, IsVolatile = false)] + /// + /// Same as Prompt, but array response spill into cells to the right. + /// + [ExcelFunction(Name = "PROMPT.TOROW", Description = _promptDescription + _structuredOutputShapeRowDescription, IsThreadSafe = true, IsVolatile = false)] public static object PromptToRow( [ExcelArgument(AllowReference = true, Name = _instructionsName, Description = _instructionsDescription)] object instructions, - [ExcelArgument(AllowReference = true, Name = _cellsOrTemperatureName, Description = _cellsOrTemperatureDescription)] object cellsOrTemperature, - [ExcelArgument(Name = _temperatureName, Description = _temperatureDescription)] object temperature) + [ExcelArgument(AllowReference = true, Name = _cellsName, Description = _cellsDescription)] params object[] ranges) { return Run( instructions, - cellsOrTemperature, - temperature, + ranges, StructuredOutputShape.Row); } -    /// -    /// Same as Prompt, but array response spill into cells below. -    /// -    [ExcelFunction(Name = "PROMPT.TOCOLUMN", Description = _promptDescription + _structuredOutputShapeColumnDescription, IsThreadSafe = true, IsVolatile = false)] + /// + /// Same as Prompt, but array response spill into cells below. + /// + [ExcelFunction(Name = "PROMPT.TOCOLUMN", Description = _promptDescription + _structuredOutputShapeColumnDescription, IsThreadSafe = true, IsVolatile = false)] public static object PromptToColumn( [ExcelArgument(AllowReference = true, Name = _instructionsName, Description = _instructionsDescription)] object instructions, - [ExcelArgument(AllowReference = true, Name = _cellsOrTemperatureName, Description = _cellsOrTemperatureDescription)] object cellsOrTemperature, - [ExcelArgument(Name = _temperatureName, Description = _temperatureDescription)] object temperature) + [ExcelArgument(AllowReference = true, Name = _cellsName, Description = _cellsDescription)] params object[] ranges) { return Run( instructions, - cellsOrTemperature, - temperature, + ranges, StructuredOutputShape.Column); } -    /// -    /// Same as Prompt, but array response spill into a rows and columns. -    /// -    [ExcelFunction(Name = "PROMPT.TORANGE", Description = _promptDescription + _structuredOutputShapeRangeDescription, IsThreadSafe = true, IsVolatile = false)] + /// + /// Same as Prompt, but array response spill into rows and columns. + /// + [ExcelFunction(Name = "PROMPT.TORANGE", Description = _promptDescription + _structuredOutputShapeRangeDescription, IsThreadSafe = true, IsVolatile = false)] public static object PromptToRange( [ExcelArgument(AllowReference = true, Name = _instructionsName, Description = _instructionsDescription)] object instructions, - [ExcelArgument(AllowReference = true, Name = _cellsOrTemperatureName, Description = _cellsOrTemperatureDescription)] object cellsOrTemperature, - [ExcelArgument(Name = _temperatureName, Description = _temperatureDescription)] object temperature) + [ExcelArgument(AllowReference = true, Name = _cellsName, Description = _cellsDescription)] params object[] ranges) { return Run( instructions, - cellsOrTemperature, - temperature, + ranges, StructuredOutputShape.Range); } -    /// -    /// Sends a prompt to the specified model. -    /// + /// + /// Sends a prompt to the specified model. + /// /// /// The model identifier. /// -    /// + /// /// The prompt to send to the model (string, cell, or cell range). /// -    /// -    /// Context cells for the prompt (cell or cell range) or a temperature value. -    /// If cells are provided, they will be used as context for the prompt. -    /// -    /// -    /// A value between 0 and 1 that controls the randomness of the model's output. -    /// Lower values make the output more deterministic, higher values make it more random. -    /// -    /// -    /// The model's response in a single cell. If an error occurs, it returns the error message. -    /// -    [ExcelFunction(Name = "PROMPTMODEL", Description = _promptModelDescription + _promptModelExample, IsThreadSafe = true, IsVolatile = false)] + /// + /// Optional cell ranges to provide as context for the prompt. + /// + /// + /// The model's response in a single cell. If an error occurs, it returns the error message. + /// + [ExcelFunction(Name = "PROMPTMODEL", Description = _promptModelDescription + _promptModelExample, IsThreadSafe = true, IsVolatile = false)] public static object PromptModel( [ExcelArgument(AllowReference = true, Name = _providerAndModelName, Description = _promptAndModelDescription)] object providerAndModel, [ExcelArgument(AllowReference = true, Name = _instructionsName, Description = _instructionsDescription)] object instructions, - [ExcelArgument(AllowReference = true, Name = _cellsOrTemperatureName, Description = _cellsOrTemperatureDescription)] object cellsOrTemperature, - [ExcelArgument(Name = _temperatureName, Description = _temperatureDescription)] object temperature) + [ExcelArgument(AllowReference = true, Name = _cellsName, Description = _cellsDescription)] params object[] ranges) { return Run( providerAndModel, instructions, - cellsOrTemperature, - temperature, + ranges, StructuredOutputShape.None); } /// /// Same as PromptModel, but array response spill into cells to the right. - /// + /// [ExcelFunction(Name = "PROMPTMODEL.TOROW", Description = _promptModelDescription + _structuredOutputShapeRowDescription, IsThreadSafe = true, IsVolatile = false)] public static object PromptModelToRow( [ExcelArgument(AllowReference = true, Name = _providerAndModelName, Description = _promptAndModelDescription)] object providerAndModel, [ExcelArgument(AllowReference = true, Name = _instructionsName, Description = _instructionsDescription)] object instructions, - [ExcelArgument(AllowReference = true, Name = _cellsOrTemperatureName, Description = _cellsOrTemperatureDescription)] object cellsOrTemperature, - [ExcelArgument(Name = _temperatureName, Description = _temperatureDescription)] object temperature) + [ExcelArgument(AllowReference = true, Name = _cellsName, Description = _cellsDescription)] params object[] ranges) { return Run( providerAndModel, instructions, - cellsOrTemperature, - temperature, + ranges, StructuredOutputShape.Row); } -    /// -    /// Same as PromptModel, but array response spill into cells below. -    /// -    [ExcelFunction(Name = "PROMPTMODEL.TOCOLUMN", Description = _promptModelDescription + _structuredOutputShapeRowDescription, IsThreadSafe = true, IsVolatile = false)] + /// + /// Same as PromptModel, but array response spill into cells below. + /// + [ExcelFunction(Name = "PROMPTMODEL.TOCOLUMN", Description = _promptModelDescription + _structuredOutputShapeRowDescription, IsThreadSafe = true, IsVolatile = false)] public static object PromptModelToColumn( [ExcelArgument(AllowReference = true, Name = _providerAndModelName, Description = _promptAndModelDescription)] object providerAndModel, [ExcelArgument(AllowReference = true, Name = _instructionsName, Description = _instructionsDescription)] object instructions, - [ExcelArgument(AllowReference = true, Name = _cellsOrTemperatureName, Description = _cellsOrTemperatureDescription)] object cellsOrTemperature, - [ExcelArgument(Name = _temperatureName, Description = _temperatureDescription)] object temperature) + [ExcelArgument(AllowReference = true, Name = _cellsName, Description = _cellsDescription)] params object[] ranges) { return Run( providerAndModel, instructions, - cellsOrTemperature, - temperature, + ranges, StructuredOutputShape.Column); } -    /// -    /// Same as PromptModel, but array responses spill into a rows and columns. -    /// -    [ExcelFunction(Name = "PROMPTMODEL.TORANGE", Description = _promptModelDescription, IsThreadSafe = true, IsVolatile = false)] + /// + /// Same as PromptModel, but array responses spill into rows and columns. + /// + [ExcelFunction(Name = "PROMPTMODEL.TORANGE", Description = _promptModelDescription, IsThreadSafe = true, IsVolatile = false)] public static object PromptModelToCell( [ExcelArgument(AllowReference = true, Name = _providerAndModelName, Description = _promptAndModelDescription)] object providerAndModel, [ExcelArgument(AllowReference = true, Name = _instructionsName, Description = _instructionsDescription)] object instructions, - [ExcelArgument(AllowReference = true, Name = _cellsOrTemperatureName, Description = _cellsOrTemperatureDescription)] object cellsOrTemperature, - [ExcelArgument(Name = _temperatureName, Description = _temperatureDescription)] object temperature) + [ExcelArgument(AllowReference = true, Name = _cellsName, Description = _cellsDescription)] params object[] ranges) { return Run( providerAndModel, instructions, - cellsOrTemperature, - temperature, + ranges, StructuredOutputShape.Range); } /// /// Forwards arguments along with the default provider and model. /// - public static object Run(object instructions, object cellsOrTemperature, object temperature, StructuredOutputShape outputShape) + public static object Run(object instructions, object[] ranges, StructuredOutputShape outputShape) { var configuration = CellmAddIn.Services.GetRequiredService(); @@ -229,15 +200,14 @@ public static object Run(object instructions, object cellsOrTemperature, object return Run( $"{provider}/{model}", instructions, - cellsOrTemperature, - temperature, + ranges, outputShape); } /// /// Parses arguments on Excel's main thread and hands off the actual work to a background thread to avoid blocking Excel's main thread. /// - public static object Run(object providerAndModel, object instructions, object cellsOrTemperature, object temperature, StructuredOutputShape outputShape) + public static object Run(object providerAndModel, object instructions, object[] ranges, StructuredOutputShape outputShape) { if (ExcelDnaUtil.IsInFunctionWizard()) { @@ -248,14 +218,13 @@ public static object Run(object providerAndModel, object instructions, object ce { var wallClock = Stopwatch.StartNew(); -            // We must parse arguments on the main thread -            var argumentParser = CellmAddIn.Services.GetRequiredService(); + // We must parse arguments on the main thread + var argumentParser = CellmAddIn.Services.GetRequiredService(); var arguments = argumentParser .AddProvider(providerAndModel) .AddModel(providerAndModel) .AddInstructions(instructions) - .AddCellsOrTemperature(cellsOrTemperature) - .AddTemperature(temperature) + .AddCells(ranges) .AddOutputShape(outputShape) .Parse(); @@ -269,7 +238,7 @@ public static object Run(object providerAndModel, object instructions, object ce // with identical arguments will reuse the response from the first call that finishes. // ExcelDNA calls this function twice. Once when invoked and once when result is ready // at which point the list of arguments is used as key to pair result with first call - new object[] { providerAndModel, instructions, cellsOrTemperature, temperature, callerCoordinates }, + new object[] { providerAndModel, instructions, ranges, callerCoordinates }, cancellationToken => GetResponseAsync(arguments, wallClock, callerCoordinates, cancellationToken)); if (response is ExcelError.ExcelErrorNA) @@ -281,24 +250,24 @@ public static object Run(object providerAndModel, object instructions, object ce } catch (ExcelErrorException ex) { -            // Short-circuit if any arguments were found to be #GETTING_DATA or contain other errors during argument parsing. -            // Excel will re-trigger this function (or already has) when inputs are updated with realized values. -            return ex.GetExcelError(); + // Short-circuit if any arguments were found to be #GETTING_DATA or contain other errors during argument parsing. + // Excel will re-trigger this function (or already has) when inputs are updated with realized values. + return ex.GetExcelError(); } catch (XlCallException) { -            // Could be many things but the only thing observed in practice is XlReturnUncalced, meaning an -            // ExcelReference's value wasn't ready yet -            return ExcelError.ExcelErrorGettingData; + // Could be many things but the only thing observed in practice is XlReturnUncalced, meaning an + // ExcelReference's value wasn't ready yet + return ExcelError.ExcelErrorGettingData; } -        // Deliberately omit catch (Exception ex) to let UnhandledExceptionHandler log unexpected exceptions -    } + // Deliberately omit catch (Exception ex) to let UnhandledExceptionHandler log unexpected exceptions + } -    /// -    /// Builds a prompt, sends it to the model, and returns the response. -    /// -    internal static async Task GetResponseAsync(Arguments arguments, Stopwatch wallClock, string callerCoordinates, CancellationToken cancellationToken) + /// + /// Builds a prompt, sends it to the model, and returns the response. + /// + internal static async Task GetResponseAsync(Arguments arguments, Stopwatch wallClock, string callerCoordinates, CancellationToken cancellationToken) { var requestClock = Stopwatch.StartNew(); @@ -310,29 +279,23 @@ public static object Run(object providerAndModel, object instructions, object ce { logger.LogInformation("Sending {caller} to {provider}/{model} ... (elapsed time: {elapsedTime}ms)", callerCoordinates, arguments.Provider, arguments.Model, wallClock.ElapsedMilliseconds); -            // Check for cancellation before doing any work -            cancellationToken.ThrowIfCancellationRequested(); + // Check for cancellation before doing any work + cancellationToken.ThrowIfCancellationRequested(); var elapsedTaskStart = wallClock.ElapsedMilliseconds; - var cells = arguments.Cells switch - { - string singleCell => singleCell, - Cells manyCells => ArgumentParser.ParseCells(manyCells), - null => "Not available", - _ => throw new ArgumentException(nameof(arguments.Cells)) - }; - var instructions = arguments.Instructions switch { - string singleCell => singleCell, - Cells manyCells => ArgumentParser.ParseCells(manyCells), + string cell => cell, + Range range => ArgumentParser.RenderRange(range), _ => throw new ArgumentException(nameof(arguments.Instructions)) }; + var ranges = ArgumentParser.RenderRanges(arguments.Ranges); + var userMessage = new StringBuilder() - .AppendLine(ArgumentParser.AddInstructions(instructions)) - .AppendLine(ArgumentParser.AddCells(cells)) + .AppendLine(ArgumentParser.FormatInstructions(instructions)) + .AppendLine(ArgumentParser.FormatRanges(ranges)) .ToString(); var cellmAddInConfiguration = CellmAddIn.Services.GetRequiredService>(); @@ -346,15 +309,15 @@ public static object Run(object providerAndModel, object instructions, object ce .AddUserMessage(userMessage) .Build(); -            // Check for cancellation before sending request -            cancellationToken.ThrowIfCancellationRequested(); + // Check for cancellation before sending request + cancellationToken.ThrowIfCancellationRequested(); var client = CellmAddIn.Services.GetRequiredService(); var response = await client.GetResponseAsync(prompt, arguments.Provider, cancellationToken).ConfigureAwait(false); var assistantMessage = response.Messages.LastOrDefault()?.Text ?? throw new InvalidOperationException("No text response"); -            // Check for cancellation before returning response -            cancellationToken.ThrowIfCancellationRequested(); + // Check for cancellation before returning response + cancellationToken.ThrowIfCancellationRequested(); logger.LogInformation("Sending {caller} to {provider}/{model} ... Done (elapsed time: {elapsedTime}ms, request time: {requestTime}ms, overhead: {overhead}ms)", callerCoordinates, arguments.Provider, arguments.Model, wallClock.ElapsedMilliseconds, requestClock.ElapsedMilliseconds, wallClock.ElapsedMilliseconds - requestClock.ElapsedMilliseconds); @@ -365,9 +328,9 @@ public static object Run(object providerAndModel, object instructions, object ce return assistantMessage; } -        // Short-circuit if any cells were found to be #GETTING_DATA or contain other errors during cell parsing. -        // Excel will re-trigger this function (or already has) when inputs are updated with realized values. -        catch (ExcelErrorException ex) + // Short-circuit if any cells were found to be #GETTING_DATA or contain other errors during cell parsing. + // Excel will re-trigger this function (or already has) when inputs are updated with realized values. + catch (ExcelErrorException ex) { return ex.GetExcelError(); } @@ -379,7 +342,7 @@ public static object Run(object providerAndModel, object instructions, object ce .LogInformation("Sending {caller} to {provider}/{model} ... Cancelled (elapsed time: {elapsedTime}ms, request time: {requestTime}ms)", callerCoordinates, arguments.Provider, arguments.Model, wallClock.ElapsedMilliseconds, requestClock.ElapsedMilliseconds); return "Cancelled"; // We must return _something_ -        } + } catch (CellmException ex) { CellmAddIn.Services @@ -390,6 +353,6 @@ public static object Run(object providerAndModel, object instructions, object ce return ex.Message; } -        // Deliberately omit catch (Exception ex) to let UnhandledExceptionHandler log unexpected exceptions. -    } + // Deliberately omit catch (Exception ex) to let UnhandledExceptionHandler log unexpected exceptions. + } } diff --git a/src/Cellm/AddIn/Cells.cs b/src/Cellm/AddIn/Cells.cs deleted file mode 100644 index 8a9f8c83..00000000 --- a/src/Cellm/AddIn/Cells.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Cellm.AddIn; - -internal record Cells(int RowFirst, int ColumnFirst, object Values); \ No newline at end of file diff --git a/src/Cellm/AddIn/Range.cs b/src/Cellm/AddIn/Range.cs new file mode 100644 index 00000000..151b3ec3 --- /dev/null +++ b/src/Cellm/AddIn/Range.cs @@ -0,0 +1,3 @@ +namespace Cellm.AddIn; + +internal record Range(int RowFirst, int ColumnFirst, object Values); \ No newline at end of file diff --git a/src/Cellm/AddIn/SystemMessages.cs b/src/Cellm/AddIn/SystemMessages.cs index 86747906..a13ff954 100644 --- a/src/Cellm/AddIn/SystemMessages.cs +++ b/src/Cellm/AddIn/SystemMessages.cs @@ -12,14 +12,14 @@ public static string SystemMessage(Provider provider, string model, DateTime now Your purpose is to provide accurate and concise responses to user prompts in Excel. The user prompts you via Cellm's =PROMPT() formula that outputs your response in a cell. The current date is {{now:yyyy-MM-dd}}. - Follow the user's instructions in the tag. Use data in the tag as context (if any). + Follow the user's instructions in the {{ArgumentParser.InstructionsBeginTag}}{{ArgumentParser.InstructionsEndTag}} tag. Use data in the {{ArgumentParser.CellsBeginTag}}{{ArgumentParser.CellsEndTag}} tag as context (if any). You follow the user's instructions in all languages, and always respond to the user in the language they use or request. - Multi-modal input and output: You can only read and write text. You do not have the ability to read or generate images or videos and you cannot read nor transcribe audio files or videos unless the user chooses to provide you multi-modal tools. - Tools: You can use tools to fetch information or perform actions if the user chooses to provide them. If available, use relevant tools: 1. When the user's instructions involves actions that you cannot perform without tools. - 2. When the user's instructions requires up-to-date information or specific data that tools can provide and that is missing from the instructions or tags. + 2. When the user's instructions requires up-to-date information or specific data that tools can provide and that is missing from the instructions or {{ArgumentParser.CellsBeginTag}}{{ArgumentParser.CellsEndTag}} tags. 3. When the user's instructions requires you to use specific tools. - Web browsing: You can browse the internet if the user chooses to provide you with web browser tools. @@ -76,7 +76,7 @@ A 2D array JSON schema is imposed on your output. - Examples: 1. Correct: [["Row1_Value1", "Row1_Value2"], ["Row2_Val1", "Row2_Value2"]] 2. Correct: [["Once upon a time there was a green bike ... , and they lived happily ever after.", "Once upon a time there was a red bike ... , and they lived happily ever after."]] (note: Using commas in prose is fine) - 3. Incorrect: [["Row1_Value1, Row1_Value2"], ["Row2_Value1", "Row2_Value2"]] + 3. Incorrect: [["Row1_Value1, Row1_Value2"], ["Row2_Value1, Row2_Value2"]] """; }