Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/ai/handler.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/ai/handler.go |
| Original SHA-256 | f86009ed777a38f844777fbc990850a43096141a0a8daa3a7165f1e79d92b96b |
| Git blob SHA | 8ea86ced711311a933fc9a4bc16b66b8fb005af0 |
| Reading-copy SHA-256 | f86009ed777a38f844777fbc990850a43096141a0a8daa3a7165f1e79d92b96b |
Every recorded occurrence
These 2 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/modules/ai/handler.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/ai/handler.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓241 lines · 9.7 KB
package ai
import (
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/missionworkforce/missionmatch/internal/platform/inference"
"github.com/missionworkforce/missionmatch/internal/platform/security"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// Handler mounts the AI brain under the tenant scoped group, behind the session resolver, CSRF, RBAC, rate
// limit, and audit (the same chain as every other module). It exposes one endpoint that routes a natural
// language question across the augmented generation strategies and returns an explainable, faithfulness
// gated, tenant isolated answer. The deterministic engine remains the floor; the brain narrates, it never
// scores.
type Handler struct {
orch *Orchestrator
gate *VectorGate // optional enforced re embed gate; nil means open (no vector store to verify)
}
// NewHandler builds the handler. orch may be nil (the brain is then reported unavailable rather than
// panicking, the graceful posture when the inference plane is unconfigured).
func NewHandler(orch *Orchestrator) *Handler { return &Handler{orch: orch} }
// WithVectorGate attaches the enforced re embed gate. When set, /ai/query refuses to serve until the
// gate confirms the stored vectors were produced by the configured embed model, so a half re embedded or
// wrong model store cannot silently mis rank a retrieval.
func (h *Handler) WithVectorGate(g *VectorGate) *Handler { h.gate = g; return h }
// Routes mounts the brain endpoint gated on the brain.query capability (the same tier as search.run).
func (h *Handler) Routes(r chi.Router) {
r.With(security.RequireCapability(security.CapBrainQuery)).Post("/ai/query", h.query)
}
// PublicRoutes mounts the unauthenticated federation read, GET or POST /mm-recall. It is deliberately NOT
// behind the session resolver or CSRF: it takes no cookie, touches no database, no tenant, and no
// orchestrator, and can only ever return the static, public, contents free knowledge base (the curated CAG
// cards, each already cited and demographic free). There is nothing to protect and nothing to forge. It is
// the World to missionMATCH federation edge: the world brain retrieves relevance ranked public knowledge,
// and the caller's own relevance gate decides what to keep. A protected attribute query returns no cards.
func (h *Handler) PublicRoutes(r chi.Router) {
r.Get("/mm-recall", h.mmRecall)
r.Post("/mm-recall", h.mmRecall)
}
// MMRecallResponse is the public federation read result: relevance ranked PUBLIC knowledge cards and
// nothing else. No tenant data, no candidate data, no demographic field ever appears here.
type MMRecallResponse struct {
Results []MMRecallItem `json:"results"`
Count int `json:"count"`
}
// MMRecallItem is one public knowledge card rendered for federation: the taught content with its cited
// source. The text field name matches what a federated caller reads.
type MMRecallItem struct {
Text string `json:"text"`
Topic string `json:"topic"`
Source string `json:"source"`
}
func (h *Handler) mmRecall(w http.ResponseWriter, r *http.Request) {
q := strings.TrimSpace(r.URL.Query().Get("q"))
k := 4
if r.Method == http.MethodPost {
var in struct {
Query string `json:"query"`
Q string `json:"q"`
Question string `json:"question"`
K int `json:"k"`
}
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<16))
if err := dec.Decode(&in); err == nil {
for _, s := range []string{in.Query, in.Q, in.Question} {
if t := strings.TrimSpace(s); t != "" {
q = t
break
}
}
if in.K > 0 {
k = in.K
}
}
}
if k < 1 || k > 8 {
k = 4
}
// Defense in depth: the knowledge base holds no demographic data, but a protected attribute or eeo
// question still returns nothing, so this edge can never be turned into a probe for one. The guard runs
// on the FULL query, before any truncation, so a protected word pushed past the length cap can never be
// scrubbed from the guard's view while still triggering a card.
lq := strings.ToLower(q)
if q == "" || MentionsProtectedAttribute(lq) || ForbidEEO(lq) != nil {
writeJSON(w, http.StatusOK, MMRecallResponse{Results: []MMRecallItem{}, Count: 0})
return
}
sq := q
if len(sq) > 400 {
sq = sq[:400]
}
cards := SelectKnowledge(sq, k)
items := make([]MMRecallItem, 0, len(cards))
for _, c := range cards {
items = append(items, MMRecallItem{Text: c.Content + " (source: " + c.Source + ")", Topic: c.Topic, Source: c.Source})
}
writeJSON(w, http.StatusOK, MMRecallResponse{Results: items, Count: len(items)})
}
// QueryRequest is the brain query. A free form question routes across CAG, RAG, and a refusal; an explicit
// intent runs the curated, parameterized, read only TAG path over the database of record. The brain never
// authors free form SQL.
type QueryRequest struct {
Question string `json:"question"`
Intent string `json:"intent,omitempty"`
Args []any `json:"args,omitempty"`
}
// AnswerResponse is the brain's answer plus its provenance: which augmented path served it, whether the
// prose was confirmed faithful or fell back to the deterministic rendering, and the evidence it was
// grounded on. A faithfulness fallback is a normal 200 with rationale_source deterministic.
type AnswerResponse struct {
Answer string `json:"answer"`
Path string `json:"path"` // cag | tag | rag | graph
RationaleSource string `json:"rationale_source"`
Citations []string `json:"citations"`
Faithful bool `json:"faithful"`
EvidenceCount int `json:"evidence_count"`
FallbackReason string `json:"fallback_reason,omitempty"`
}
func (h *Handler) query(w http.ResponseWriter, r *http.Request) {
if h.orch == nil {
writeErr(w, http.StatusServiceUnavailable, "brain_unavailable", "the brain is not configured")
return
}
// The enforced re embed gate: refuse to serve a query over a vector store that does not match the
// configured embed model, before any work, so a wrong model store never silently mis ranks.
if h.gate != nil {
if err := h.gate.Check(r.Context()); err != nil {
mapErr(w, err)
return
}
}
var in QueryRequest
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
dec.DisallowUnknownFields()
if err := dec.Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "bad_request", "invalid request body")
return
}
if strings.TrimSpace(in.Question) == "" && strings.TrimSpace(in.Intent) == "" {
writeErr(w, http.StatusUnprocessableEntity, "validation", "a question or an intent is required")
return
}
resp, err := h.orch.Answer(r.Context(), in.Question, in.Intent, in.Args)
if err != nil {
mapErr(w, err)
return
}
if resp.Refused {
// An unanswerable, unsafe, or out of allowlist question is a typed 4xx, never a raw error.
writeErr(w, http.StatusUnprocessableEntity, "refused", resp.RefusalReason)
return
}
writeJSON(w, http.StatusOK, AnswerResponse{
Answer: resp.Answer,
Path: pathOf(resp.Route),
RationaleSource: resp.RationaleSource,
Citations: resp.Citations,
Faithful: resp.Faithful,
EvidenceCount: resp.EvidenceCount,
FallbackReason: resp.FallbackReason,
})
}
// pathOf maps the internal route to the augmented path name the caller sees.
func pathOf(route Route) string {
switch route {
case RouteTAG:
return "tag"
case RouteRAG:
return "rag"
case RouteGraph:
return "graph"
default:
return "cag"
}
}
// mapErr maps a brain error to the typed envelope. It NEVER returns a raw SQL or database error string.
func mapErr(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrNoModel):
writeErr(w, http.StatusServiceUnavailable, "brain_unavailable", "the inference plane is not configured")
case errors.Is(err, ErrVectorModelMismatch):
writeErr(w, http.StatusServiceUnavailable, "vector_model_mismatch", "the stored vectors do not match the configured embed model; the brain is disabled until the store is re embedded")
case errors.Is(err, inference.ErrCircuitOpen), errors.Is(err, inference.ErrPlaneUnavailable):
writeErr(w, http.StatusServiceUnavailable, "plane_unavailable", "the inference plane is temporarily unavailable")
case errors.Is(err, ErrAuditFailed):
// The brain could answer, but the AEDT record of that answer could not be written, so the answer is
// not served. It is a 503 and not a 4xx because the caller's request was fine: this is the platform
// declining to make an automated employment decision it cannot account for, and a retry once the
// audit path recovers is the correct client behaviour.
writeErr(w, http.StatusServiceUnavailable, "audit_unavailable", "the decision could not be recorded, so it was not served")
case errors.Is(err, ErrUnknownIntent):
writeErr(w, http.StatusUnprocessableEntity, "unknown_intent", "no such query intent")
case errors.Is(err, ErrEEOForbidden):
writeErr(w, http.StatusUnprocessableEntity, "refused", "the question references prohibited demographic data")
case errors.Is(err, tenant.ErrNoTenant):
writeErr(w, http.StatusForbidden, "no_tenant", "no validated tenant")
default:
// Any other failure (a bad intent argument, a database error) is reported generically so no raw SQL
// or internal detail leaks to the caller.
writeErr(w, http.StatusBadRequest, "brain_error", "the brain could not answer the question")
}
}
type errorBody struct {
Error errorEnvelope `json:"error"`
}
type errorEnvelope struct {
Code string `json:"code"`
Message string `json:"message"`
}
func writeErr(w http.ResponseWriter, status int, code, msg string) {
writeJSON(w, status, errorBody{errorEnvelope{Code: code, Message: msg}})
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}