mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Allow assistant to evaluate current formula
Summary: Replaces https://phab.getgrist.com/D3940, particularly to avoid doing potentially unwanted things automatically. Adds optional fields `evaluateCurrentFormula?: boolean; rowId?: number` to `FormulaAssistanceContext` (part of `AssistanceRequest`). When `evaluateCurrentFormula` is `true`, calls a new function `evaluate_formula` in the sandbox which computes the existing formula in the column (regardless of anything the AI may have suggested) and uses that to generate an additional system message which is added before the user's message. In theory this could be used in an interface where users ask why a formula doesn't work, including possibly a formula suggested by the AI. For now, it's only used in `runCompletion_impl.ts` for experimenting. Also cleaned up a bit, removing `_chatMode` which is always `true` now, and uses of `regenerate` which is always `false`. Test Plan: Updated `runCompletion_impl` to optionally use the new feature, in which case it now scores 51/68 instead of 49/68. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3970
This commit is contained in:
59
sandbox/grist/attribute_recorder.py
Normal file
59
sandbox/grist/attribute_recorder.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from six.moves import reprlib
|
||||
|
||||
import records
|
||||
|
||||
|
||||
class AttributeRecorder(object):
|
||||
"""
|
||||
Wrapper around a Record that records attribute accesses.
|
||||
Used to generate a prompt for the AI with basic 'debugging' info.
|
||||
"""
|
||||
def __init__(self, inner, name, attributes):
|
||||
assert isinstance(inner, records.Record)
|
||||
self._inner = inner
|
||||
self._name = name
|
||||
self._attributes = attributes
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Record attribute access.
|
||||
If the result is a Record or RecordSet, wrap that with AttributeRecorder
|
||||
to also record nested attribute values.
|
||||
"""
|
||||
result = getattr(self._inner, name)
|
||||
full_name = "{}.{}".format(self._name, name)
|
||||
if isinstance(result, records.Record):
|
||||
result = AttributeRecorder(result, full_name, self._attributes)
|
||||
elif isinstance(result, records.RecordSet):
|
||||
# Use a tuple to imply immutability so that the AI doesn't try appending.
|
||||
# Don't try recording attributes of all contained records, just record the first access.
|
||||
# Pretend that the attribute is always accessed from the first record for simplicity.
|
||||
result = tuple(AttributeRecorder(r, full_name + "[0]", self._attributes) for r in result)
|
||||
self._attributes.setdefault(full_name, safe_repr(result))
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
# The usual Record repr looks like Table1[2] which may surprise the AI.
|
||||
return "{}(id={})".format(self._inner._table.table_id, self._inner._row_id)
|
||||
|
||||
|
||||
arepr = reprlib.Repr()
|
||||
arepr.maxlevel = 3
|
||||
arepr.maxtuple = 3
|
||||
arepr.maxlist = 3
|
||||
arepr.maxarray = 3
|
||||
arepr.maxdict = 4
|
||||
arepr.maxset = 3
|
||||
arepr.maxfrozenset = 3
|
||||
arepr.maxdeque = 3
|
||||
arepr.maxstring = 40
|
||||
arepr.maxlong = 20
|
||||
arepr.maxother = 60
|
||||
|
||||
|
||||
def safe_repr(x):
|
||||
try:
|
||||
return arepr.repr(x)
|
||||
except Exception:
|
||||
# Copied from Repr.repr_instance in Python 3.
|
||||
return '<%s instance at %#x>' % (x.__class__.__name__, id(x))
|
||||
@@ -20,6 +20,7 @@ from sortedcontainers import SortedSet
|
||||
import acl
|
||||
import actions
|
||||
import action_obj
|
||||
from attribute_recorder import AttributeRecorder
|
||||
from autocomplete_context import AutocompleteContext, lookup_autocomplete_options, eval_suggestion
|
||||
from codebuilder import DOLLAR_REGEX
|
||||
import depend
|
||||
@@ -694,22 +695,27 @@ class Engine(object):
|
||||
not recomputing the whole column and dependent columns as well. So it recomputes the formula
|
||||
for this cell and returns error message with details.
|
||||
"""
|
||||
result = self.get_formula_value(table_id, col_id, row_id)
|
||||
table = self.tables[table_id]
|
||||
col = table.get_column(col_id)
|
||||
# If the error is gone for a trigger formula
|
||||
if col.has_formula() and not col.is_formula():
|
||||
if not isinstance(result, objtypes.RaisedException):
|
||||
# Get the error stored in the cell
|
||||
# and change it to show to the user that no traceback is available
|
||||
error_in_cell = objtypes.decode_object(col.raw_get(row_id))
|
||||
assert isinstance(error_in_cell, objtypes.RaisedException)
|
||||
return error_in_cell.no_traceback()
|
||||
return result
|
||||
|
||||
def get_formula_value(self, table_id, col_id, row_id, record_attributes=None):
|
||||
table = self.tables[table_id]
|
||||
col = table.get_column(col_id)
|
||||
checkpoint = self._get_undo_checkpoint()
|
||||
# Makes calls to REQUEST synchronous, since raising a RequestingError can't work here.
|
||||
self._sync_request = True
|
||||
try:
|
||||
result = self._recompute_one_cell(table, col, row_id)
|
||||
# If the error is gone for a trigger formula
|
||||
if col.has_formula() and not col.is_formula():
|
||||
if not isinstance(result, objtypes.RaisedException):
|
||||
# Get the error stored in the cell
|
||||
# and change it to show to the user that no traceback is available
|
||||
error_in_cell = objtypes.decode_object(col.raw_get(row_id))
|
||||
assert isinstance(error_in_cell, objtypes.RaisedException)
|
||||
return error_in_cell.no_traceback()
|
||||
return result
|
||||
return self._recompute_one_cell(table, col, row_id, record_attributes=record_attributes)
|
||||
finally:
|
||||
# It is possible for formula evaluation to have side-effects that produce DocActions (e.g.
|
||||
# lookupOrAddDerived() creates those). In case of get_formula_error(), these aren't fully
|
||||
@@ -920,7 +926,7 @@ class Engine(object):
|
||||
|
||||
raise RequestingError()
|
||||
|
||||
def _recompute_one_cell(self, table, col, row_id, cycle=False, node=None):
|
||||
def _recompute_one_cell(self, table, col, row_id, cycle=False, node=None, record_attributes=None):
|
||||
"""
|
||||
Recomputes an one formula cell and returns a value.
|
||||
The value can be:
|
||||
@@ -939,6 +945,11 @@ class Engine(object):
|
||||
|
||||
checkpoint = self._get_undo_checkpoint()
|
||||
record = table.Record(row_id, table._identity_relation)
|
||||
if record_attributes is not None:
|
||||
assert isinstance(record_attributes, dict)
|
||||
assert col.is_formula()
|
||||
assert not cycle
|
||||
record = AttributeRecorder(record, "rec", record_attributes)
|
||||
value = None
|
||||
try:
|
||||
if cycle:
|
||||
|
||||
@@ -7,6 +7,9 @@ import asttokens
|
||||
import asttokens.util
|
||||
import six
|
||||
|
||||
import attribute_recorder
|
||||
import objtypes
|
||||
from codebuilder import make_formula_body
|
||||
from column import is_visible_column, BaseReferenceColumn
|
||||
from objtypes import RaisedException
|
||||
import records
|
||||
@@ -255,3 +258,25 @@ def convert_completion(completion):
|
||||
result = asttokens.util.replace(result, replacements)
|
||||
|
||||
return result.strip()
|
||||
|
||||
|
||||
def evaluate_formula(engine, table_id, col_id, row_id):
|
||||
grist_formula = engine.docmodel.get_column_rec(table_id, col_id).formula
|
||||
assert grist_formula
|
||||
plain_formula = make_formula_body(grist_formula, default_value=None).get_text()
|
||||
|
||||
attributes = {}
|
||||
result = engine.get_formula_value(table_id, col_id, row_id, record_attributes=attributes)
|
||||
if isinstance(result, objtypes.RaisedException):
|
||||
name, message = result.encode_args()[:2]
|
||||
result = "%s: %s" % (name, message)
|
||||
error = True
|
||||
else:
|
||||
result = attribute_recorder.safe_repr(result)
|
||||
error = False
|
||||
return dict(
|
||||
error=error,
|
||||
formula=plain_formula,
|
||||
result=result,
|
||||
attributes=attributes,
|
||||
)
|
||||
|
||||
@@ -153,6 +153,10 @@ def run(sandbox):
|
||||
def convert_formula_completion(completion):
|
||||
return formula_prompt.convert_completion(completion)
|
||||
|
||||
@export
|
||||
def evaluate_formula(table_id, col_id, row_id):
|
||||
return formula_prompt.evaluate_formula(eng, table_id, col_id, row_id)
|
||||
|
||||
export(parse_acl_formula)
|
||||
export(eng.load_empty)
|
||||
export(eng.load_done)
|
||||
|
||||
Reference in New Issue
Block a user