Documentation
¶
Overview ¶
Package ask is a plain-language question about your business, answered with real numbers.
It is the UNIFIED GROUNDED ADVISOR behind POST /v1/ask. A founder asks a plain-language question ("what's my MRR?", "how long is my runway?") and gets an answer whose every figure is a REAL value read from a domain endpoint in-process — never a number the model invented.
ONE AND ONE WAY. /v1/ask is DISTINCT from /v1/chat/completions (the ai subsystem's RAW model completions) and from /v1/agent (the tool-calling orchestrator). Raw model → /v1/chat/completions; grounded advisor → /v1/ask. The advisor routes a question to the domain(s) that can ground it, reads the REAL figures from each domain's own endpoint in-process, hands the model the EXACT figures, and returns the grounded answer + the figures + the domain reads that backed them.
THE FLOW.
question → registry.Match (which domain grounds this?) → Contributor.Gather (replay the
domain's grounded READ in-process, under the caller's OWN creds) → the REAL facts
→ narrate the facts with the model (prose only, never a number) → {answer, figures,
followups, sources, domain}
GROUNDING CONTRACT (non-negotiable). Every figure in the answer is a real domain read; the model only NARRATES the figures it is handed and can never override one — the figures array is the Contributor's, computed BEFORE any model call and returned unaltered. If no domain can ground the question, the advisor says so honestly rather than guessing. Per-tenant isolation is inherited from the in-process replay carrying the caller's creds (agent.go's pattern): a question can only ever surface the caller's own org's data.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Mount ¶
Mount wires POST /v1/ask into cloud, building the contributor registry (books today) over the SAME app so a contributor's Gather replays a domain's grounded read in-process. The narration model comes from deps.AI. Mount is a distinct route, so it wins Fiber's first-match over the ai /v1/* catch-all.
Types ¶
type Contributor ¶
type Contributor interface {
Name() string
CanAnswer(question string) bool
Gather(ctx context.Context, cred map[string]string) (facts []Fact, sources []string, err error)
}
Contributor is one grounded domain behind the advisor. It is the ONE plug-in seam: a domain declares WHICH questions it can ground (CanAnswer) and HOW it reads the real figures (Gather), and the router composes it with no domain-specific branch of its own.
- Name reports the domain id surfaced as the answer's `domain` and used in traces.
- CanAnswer is the lightweight classifier: does this domain ground THIS question? Keyword/ intent today; an LLM classifier can replace the body without changing the seam.
- Gather reads the REAL figures IN-PROCESS under the caller's own creds (the replayable header set), returning the facts and the domain reads (sources) that backed them. It never writes and never fabricates: an empty/zero domain yields honest zero figures.
type Fact ¶
type Fact struct {
Label string `json:"label"`
Value string `json:"value"`
Period string `json:"period,omitempty"`
}
Fact is one grounded figure a Contributor read from its domain: a label, its formatted value, and the period it covers. It is the exact {label,value,period?} shape the /v1/ask answer surfaces — the model narrates these, it never produces one.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the ordered set of contributors the router consults. It is populated once at Mount and read at request time, so adding a domain is a one-line append at the composition point — never a router edit.
func NewRegistry ¶
func NewRegistry(cs ...Contributor) *Registry
NewRegistry builds the registry from the contributors wired at Mount, in priority order (first match wins the classification).
func (*Registry) Match ¶
func (r *Registry) Match(question string) Contributor
Match returns the FIRST contributor that can ground the question, or nil when none can. It is the classification hook: deterministic first-match today (books is the sole domain), and the ONE place an LLM-based classifier or a multi-domain fan-out slots in later — the router only ever asks the registry "who can answer this?", never how the decision is made.
func (*Registry) Register ¶
func (r *Registry) Register(c Contributor)
Register appends a contributor — the plug-in point a new domain calls to join the advisor without the router knowing it exists.