Documentation
¶
Overview ¶
Package feelgen writes a FEEL expression from a conversation with the author (ADR-0445).
It is design-time authoring, the way form generation is (ADR-0260), and it stands on the same three legs. There is no instance, no token, no job and no event: an author says what an expression should compute, a model writes one, and it arrives in the console's FEEL assistant as a proposal the author tests, copies or applies to a field themselves. Nothing here writes to a store.
**It asks the Worker an operator already configured.** An agent Worker (ADR-0255) is a Console record holding an endpoint, a wire format, a model name and a vault reference to an API key; generation reaches exactly that record, through the adapters the runtime agent uses (connector/agent). There is no second place to configure a model, no second credential, and no key in the browser.
**What the model writes is checked by the engine that will run it.** That is the one thing this package adds to form generation's pattern, and the reason it can run on a small free model. Every expression is compiled, its calls checked as a deploy checks them (ADR-0388), and evaluated against the example the model gave, with the result held against the one the model claimed. An answer that fails goes back to the model with the engine's own words, a bounded number of times; only then does the author see it — with the engine's verdict beside it, whichever way it went.
**The conversation is the console's, not this package's.** The adapters send one message per call, and nothing here remembers a request after answering it: the console sends the whole conversation each time, and the reply it should keep as the model's turn comes back with every answer.
Index ¶
Constants ¶
const ( // ResultSettled: an expression that runs, binds its inputs, and returns what the // model said it returns. ResultSettled = "settled" // ResultQuestion: the model asked back instead of writing an expression. ResultQuestion = "question" // ResultUnsettled: every round was used and the last answer still failed its check. ResultUnsettled = "unsettled" // ResultCutShort: a correction round could not run — the endpoint refused it, or // answered with nothing usable — so the author got an earlier, failed answer. ResultCutShort = "cut_short" // ResultUnanswered: the first round produced nothing usable. ResultUnanswered = "unanswered" )
The results a request can come to. Each is a label value of the server's request counter, so the set is closed (ADR-0142's bounded-cardinality rule).
const ( FaultNone = "none" FaultUnusable = "unusable" // no answer, an endpoint error, or an answer with nothing in it FaultEmpty = "empty" // an answer with neither an expression nor an explanation FaultCompile = "compile" FaultCalls = "calls" // a call the deploy would refuse (ADR-0388) FaultEvaluate = "evaluate" // compiled, and failed while evaluating FaultMissing = "missing" // reads an input the example does not bind FaultMismatch = "mismatch" // returns something other than what the model said )
The faults a round can have, in the order the check looks for them.
const ( FormatContract = "contract" FormatCodeBlock = "code_block" FormatProse = "prose" FormatUnusable = "unusable" )
The forms an answer can take (see ParseAnswer). How often a model keeps to the contract is the first thing a change to the prompt's wording moves.
const PromptVersion = "1"
PromptVersion names the prompt this build sends: the system prompt, the shape of a goal and the shape of a correction round together. It travels with every answer and every measurement (Outcome), because the measurements are how the prompt is improved, and the measurements of two prompts must stay apart to be compared.
Raise it with every change to what a model is told. TestThePromptVersionNamesThePrompt holds it to promptFingerprint and fails on a change that did not raise it — including one nobody made in this package, when a dependency update changes the engine's function list and with it the prompt.
Variables ¶
var ( Results = []string{ResultSettled, ResultQuestion, ResultUnsettled, ResultCutShort, ResultUnanswered} Faults = []string{FaultNone, FaultUnusable, FaultEmpty, FaultCompile, FaultCalls, FaultEvaluate, FaultMissing, FaultMismatch} Formats = []string{FormatContract, FormatCodeBlock, FormatProse, FormatUnusable} )
Results, Faults and Formats are the closed lists the server pre-resolves its metrics from. TestTheLabelsAreClosed holds them to the constants above.
Functions ¶
This section is empty.
Types ¶
type Attempt ¶
type Attempt struct {
Format string
Fault string
// Error is the engine's verdict, or the reason the round produced nothing usable.
// Empty for a round without a fault.
Error string
// Calls are the callees the check refused, by name — the foreign functions a
// model reached for.
Calls []string
}
Attempt is one round.
type Capability ¶
Capability is what the console asks before it offers to write anything.
type Check ¶
type Check struct {
// OK is true when the expression compiled, every call in it is one this build
// can make (ADR-0388), and it evaluated. It says nothing about whether the result
// is the right one; Matches does.
OK bool `json:"ok"`
// Result and Kind are the value as the FEEL evaluate route reports it: the
// canonical text and its kind's label (expr.ValueKind.Label).
Result string `json:"result,omitempty"`
Kind string `json:"kind,omitempty"`
Error string `json:"error,omitempty"`
// Fault is the kind of failure when OK is false: FaultCompile, FaultCalls or
// FaultEvaluate. Calls names the callees a FaultCalls refused; it is reported to
// whoever measures the prompt (Outcome), not to the console, whose author reads
// the same names in Error.
Fault string `json:"fault,omitempty"`
Calls []string `json:"-"`
// Inputs are the variables the expression reads, and Missing those of them the
// example does not bind. A missing input reads as null, which makes the example
// prove nothing about it.
Inputs []string `json:"inputs,omitempty"`
Missing []string `json:"missing,omitempty"`
// Expected is what the model said the expression returns, as JSON text, and
// Matches whether the engine agrees. Both are absent when it said nothing.
Expected string `json:"expected,omitempty"`
Matches *bool `json:"matches,omitempty"`
}
Check is what the engine said about a proposal: whether it compiles into something this build can run, what it returns for the example, and whether that is what the model claimed it returns.
It is the reason this feature can run on a free model. A model's FEEL is a guess at a dialect it learnt from somebody else's documentation; the check is the engine that will actually run the expression saying whether the guess was right, before the author sees it — and, when it was not, in words the next correction round can act on.
func Evaluate ¶
Evaluate checks an expression against example variables, and against the result the model stated when hasExpected is true. vars is decoded JSON with exact numbers (json.Number), the shape the request and ParseAnswer both produce.
It is a pure compile and evaluation over the caller's values — no engine state is read or written — exactly as the FEEL evaluate route is, and it refuses what the FEEL validate route refuses, so the check an answer passed is the one the author's own editor would have run.
type Outcome ¶
type Outcome struct {
Worker string
// Model is the language model that was asked: the Worker's own, or the one the
// request chose. Comparing models is the second thing the data is for.
Model string
// Prompt is the PromptVersion that was sent.
Prompt string
Result string
Attempts []Attempt
Duration time.Duration
}
Outcome is one request that reached a model.
type Proposal ¶
type Proposal struct {
Expression string
Explanation string
// Variables is the model's example, decoded with exact numbers. Nil when it gave
// none, or gave something that is not an object and so cannot bind by name.
Variables map[string]any
// Expected is what the model says the expression returns for Variables, and
// HasExpected whether it said anything at all — "expected": null is a claim too.
Expected any
HasExpected bool
// Format is which reading of the answer succeeded: the contract, a code block, or
// plain prose (FormatContract, FormatCodeBlock, FormatProse).
Format string
}
Proposal is one answer of the model, read into the contract the system prompt asks for: an expression, an explanation, the example it was written against and what it returns for that example. Any part may be missing — a question back has no expression, a bare code block has no example — and the check decides what that means.
func ParseAnswer ¶
ParseAnswer reads what a model wrote into a Proposal. It forgives as much as can be forgiven without guessing, because the models an installation points this at are not all disciplined about a format:
- the contract as a JSON object, bare, in a fence, or with prose around it;
- failing that, an expression in a code block, with the prose around it as the explanation;
- failing that, plain prose, which is the model talking — a question back, or a refusal — and is shown as that, with no expression.
What it does not forgive is an answer longer than max or one with nothing in it.
type Request ¶
type Request struct {
// Messages is the conversation, oldest first. The last one is the author's and is
// the one to answer.
Messages []Turn `json:"messages"`
// Expression is what the assistant's editor holds now, which the author may have
// changed by hand since the last answer.
Expression string `json:"expression,omitempty"`
// Variables are the editor's test variables, a JSON object. They are shown to the
// model and are what an answer without an example of its own is checked against.
Variables json.RawMessage `json:"variables,omitempty"`
// Target says where the expression will be used — the field the assistant was
// opened from, in the console's words — because a gateway condition must be a
// boolean and a decision table's input cell is not an expression at all.
Target string `json:"target,omitempty"`
// Worker names the agent Worker to ask; empty picks the only one in reach.
Worker string `json:"worker,omitempty"`
// Model overrides the language model that Worker is configured for (ADR-0256).
Model string `json:"model,omitempty"`
}
Request is one message of the author's, with everything said before it.
type Response ¶
type Response struct {
Expression string `json:"expression"`
Explanation string `json:"explanation"`
// Variables is the example the expression was checked against — the model's
// own, or the author's when it gave none — for the console to load beside it.
Variables json.RawMessage `json:"variables,omitempty"`
// Check is the engine's verdict, absent when the answer was a question back.
Check *Check `json:"check,omitempty"`
// Reply is the model's turn as the console should keep it and send it back.
Reply string `json:"reply"`
// Attempts is how many times the model was asked: one, plus each correction.
Attempts int `json:"attempts"`
// Warning says why a correction round could not run, when one could not — the
// proposal is then the best answer the model had given.
Warning string `json:"warning,omitempty"`
Worker string `json:"worker"`
Model string `json:"model,omitempty"`
// Prompt is the PromptVersion that asked: an author comparing two answers across
// an upgrade can tell whether the prompt changed between them.
Prompt string `json:"prompt"`
}
Response is the proposal, with the engine's verdict on it. Nothing is stored.
type Service ¶
type Service struct {
// Observe, when set, is told what every request that reached a model came to
// (Outcome). The server logs it and counts it; nil measures nothing.
Observe func(Outcome)
// Limits are the installation's resource budgets. New sets them to
// [limits.Default]; the server overwrites them with its own (ADR-0291).
Limits limits.Limits
// contains filtered or unexported fields
}
Service writes FEEL expressions. Build it with New.
func New ¶
func New(workers func(*http.Request) ([]Worker, error), dial func(*http.Request, string) (agent.Model, error)) *Service
New builds the service over the server's two collaborators.
func (*Service) Generate ¶
Generate answers the author's last message.
The model call happens here, on the caller's goroutine, under the request's own context — an author who closes the assistant takes their request with them, and a model endpoint that hangs costs one request rather than the engine (ADR-0260). status is the HTTP status an error should reach the author as.
func (*Service) HandleCapability ¶
func (s *Service) HandleCapability(w http.ResponseWriter, r *http.Request)
HandleCapability answers whether there is an AI Worker to ask, and which ones this principal may name. The assistant asks first so that its chat can be absent rather than broken: without an AI Worker the editor, the test pane, the history and the favourites still work, and only the conversation is missing.
func (*Service) HandleGenerate ¶
func (s *Service) HandleGenerate(w http.ResponseWriter, r *http.Request)
HandleGenerate answers one message of the conversation with a checked proposal.
Nothing is stored: the expression goes to the assistant's editor, where the author runs it, changes it, copies it or applies it to a field — and that field's own save path, with its own checks, is what puts it into a model.
type Turn ¶
Turn is one message of the conversation, the author's ("user") or the model's ("assistant"). The model's turns are the Reply of earlier responses, kept by the console as they came.
type Worker ¶
type Worker struct {
Name string `json:"name"`
Model string `json:"model,omitempty"`
Provider string `json:"provider,omitempty"`
}
Worker is one agent Worker as this area needs to speak about it: the name a request names it by, and what it is configured to ask. No endpoint and nowhere to put a credential (ADR-0041/0069).