Class ModelAwareSystemTool<T extends Record & ModelInput>

java.lang.Object
com.axelor.ai.tool.system.ModelAwareSystemTool<T>
All Implemented Interfaces:
SystemTool<T>
Direct Known Subclasses:
CreateRecordTool, DeleteRecordTool, DescribeModelTool, ReadActivityTool, ReadRecordTool, SearchRecordsTool, UpdateRecordTool

public abstract class ModelAwareSystemTool<T extends Record & ModelInput> extends Object implements SystemTool<T>
Abstract base for system tools that operate on a specific model. Handles model name resolution, the model check — the user's permissions and the agent's, through Access — and field projection.

Supports both JPA entities and custom models (MetaJsonModel / MetaJsonRecord). The tool detects the model type internally: if JPA.model() resolves the name, the JPA entity path is used; otherwise the tool looks up a custom model via MetaJsonModelRepository.

  • Constructor Details

    • ModelAwareSystemTool

      public ModelAwareSystemTool()
  • Method Details

    • operation

      protected abstract Operation operation()
      The operation this tool performs, which the model's permissions are checked for.
    • execute

      public final ToolResult execute(T input, ToolContext context)
      Description copied from interface: SystemTool
      Execute with the given typed input and agent-scoped context.
      Specified by:
      execute in interface SystemTool<T extends Record & ModelInput>
    • beforeApproval

      public ToolResult beforeApproval(T input, ToolContext context)
      Refuses, before the user is asked to approve the call, what running it would refuse: the operation on the model, and what checkForModel(T, java.lang.Class<? extends com.axelor.db.Model>, com.axelor.ai.governance.access.Access) finds. Null asks as usual.
      Specified by:
      beforeApproval in interface SystemTool<T extends Record & ModelInput>
      Parameters:
      input - the call's input, or null when the tool takes none
      context - the call's context
    • executeForModel

      protected abstract ToolResult executeForModel(T input, Class<? extends Model> modelClass, Access access)
      Subclass hook — called for a JPA entity once the model is allowed.
      Parameters:
      access - what the call may do; the tool asks it about every record and field it touches, and may throw AccessDenied from anywhere to refuse the call
    • executeForJsonModel

      protected abstract ToolResult executeForJsonModel(T input, MetaJsonModel jsonModel, Access access)
      Subclass hook — called for a custom model once it is allowed.
    • checkForModel

      protected ToolResult checkForModel(T input, Class<? extends Model> modelClass, Access access)
      What the call would refuse on this entity, found without writing anything, so that a refusal known up front costs the user no approval; null when it would go through. The call checks again when it runs.
    • checkForJsonModel

      protected ToolResult checkForJsonModel(T input, MetaJsonModel jsonModel, Access access)
      The same for a custom model.
    • saveRecord

      protected Map<String,Object> saveRecord(Class<? extends Model> modelClass, Map<String,Object> values, Model attrsFrom, Projection projection)
      Writes values to a record of this model and answers with what identifies it.

      Custom fields are separated out and merged into the JSON attrs of attrsFrom — the record being updated, or the new bean when one is being created — so that writing one custom field leaves the rest of them alone. The answer is the record's identity as projection reads it: its name field withheld when the user may not read it.

    • validateFields

      protected static void validateFields(Class<? extends Model> modelClass, List<String> fields, Projection projection)
      Refuses names this entity cannot project, before a page of records is built from them.

      A name the projection does not resolve is simply absent from every record it answers with, which reads as "this field is empty" when it only means the name was wrong — and on a search that matched nothing, it is never noticed at all. Checking the ask rather than each answer also costs one lookup instead of one per record.

      A dotted path is refused too: the projection reads one property, so customer.name would resolve to nothing. Naming the relation itself answers with {id, name}.

    • validateJsonFields

      protected static void validateJsonFields(MetaJsonModel jsonModel, List<String> fields, Projection projection)
      The same, for the names a custom model defines.
    • invalidValues

      protected static String invalidValues(Class<? extends Model> modelClass, Map<String,Object> values, Projection projection)
      What is wrong with a write, or null when nothing is.

      A name the entity does not define is dropped on the way to the database — JPA.edit skips what it cannot map — and the write then answers with the saved record, so a misspelled field reads as a value that was stored. Refusing the call is the only way the caller learns that it was not.

      A value written to a field limited to a set of values is checked against that set for the same reason: an unknown one is stored as written, and reads back as data.

      Nested values are checked against the model they are written to, so an item of a one-to-many is held to that model's names rather than the parent's.

    • invalidJsonValues

      protected static String invalidJsonValues(MetaJsonModel jsonModel, Map<String,Object> values, Projection projection)
      The same, for a write to a custom model, whose names are the ones it defines.
    • invalidChoice

      protected static String invalidChoice(String modelName, String fieldName, Object value, List<Selection.Option> options)
      What is wrong with a value for a field limited to a set of values, or null when nothing is.

      Nothing refuses a value outside the set: a selection is a string column, so a wrong one is stored as written, and a wrong one in a filter matches no row and reads as "there are none" — which is the same answer the caller would get if there really were none. The values the field takes are named, because guessing one is exactly what went wrong.

      A value may also be several, for a multi-select field, which stores its choices as one comma-separated string, or for a criterion matching against a list. The whole value is tried first, so a single choice that contains a comma is still recognised.

    • selectionOptions

      protected static List<Selection.Option> selectionOptions(Property prop)
      The options a field is limited to, empty when it is not limited to any.

      One source for what a field accepts, so that what describe_model tells the caller and what a write or a filter is held to cannot say different things.

    • selectionOptions

      protected static List<Selection.Option> selectionOptions(MetaJsonField field)
      The same, for a custom field, which carries a named selection or none.
    • customFieldsByName

      protected static Map<String,MetaJsonField> customFieldsByName(Class<? extends Model> modelClass)
      An entity's custom fields, by name.
    • entityFieldNames

      public static Set<String> entityFieldNames(Class<? extends Model> modelClass)
      The names an entity accepts: its own properties, its custom fields, id and version.
    • jsonFieldNames

      public static Set<String> jsonFieldNames(MetaJsonModel jsonModel)
      The names a custom model defines — what a caller may pass in fields or orderBy.
    • jsonModelFields

      protected static List<MetaJsonField> jsonModelFields(MetaJsonModel jsonModel)
      The fields a custom model defines, which is none rather than null when it defines none.
    • unknownJsonFields

      protected static IllegalArgumentException unknownJsonFields(MetaJsonModel jsonModel, List<String> unknown, Projection projection)
      The error for names the custom model does not define, listing the ones it does that the user may read.

      Shared so that a misspelling reads the same whether it reached us through a projection or a sort, and always names the custom model rather than the MetaJsonRecord it is stored in.

    • unknownFields

      protected static IllegalArgumentException unknownFields(String modelName, Collection<String> known, Collection<String> unknown)
      The error for names a model does not define, listing the ones it does.

      The names are what lets the caller correct itself on the next call instead of guessing again, which is the whole reason this is an error rather than an empty answer. A caller lists only the names the user may read: the rest are not theirs to learn of.

    • unknownFieldsMessage

      protected static String unknownFieldsMessage(String modelName, Collection<String> known, Collection<String> unknown)
      The same, for a caller that reports rather than throws.
    • jsonPropertyOf

      protected static String jsonPropertyOf(Class<? extends Model> modelClass)
      Returns the name of the JSON property on the entity (usually "attrs"), or null if none.
    • entityCustomFields

      protected static List<MetaJsonField> entityCustomFields(Class<? extends Model> modelClass)
      Queries custom field definitions for an entity's JSON property.
    • entityCustomFieldNames

      protected static Set<String> entityCustomFieldNames(Class<? extends Model> modelClass)
      Returns the set of custom field names for an entity.
    • parseAttrs

      protected static Map<String,Object> parseAttrs(Object attrsValue)
      Parses a JSON attrs string to a map. Returns null on failure.
    • serializeAttrs

      protected static String serializeAttrs(Map<String,Object> attrs)
      Serializes an attrs map to a JSON string.
    • extractCustomFields

      protected static Map<String,Object> extractCustomFields(Map<String,Object> data, Class<? extends Model> modelClass)
      Separates custom field values from a data map. Returns the custom field values that were removed from the data map, or null if none.