Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/ai/orchestrator.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 43d56387270ef4da4cabd794d97ca6a49909bc7d |
| Branch | w3-ai-staffing-recommendations |
| Path or archive member | internal/modules/ai/orchestrator.go |
| Original SHA-256 | 41d38b7549f5c9893d09c88a762f753bc0dabf4fd4586cb7a4568616e61044a3 |
| Git blob SHA | 7bcabecb15e15450f511e8b95ae0ea45698aa9bf |
| Reading-copy SHA-256 | 41d38b7549f5c9893d09c88a762f753bc0dabf4fd4586cb7a4568616e61044a3 |
Every recorded occurrence
These 3 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/orchestrator.go | w3-ai-staffing-recommendations | 43d56387270ef4da4cabd794d97ca6a49909bc7d | Open original |
| Janady13/missionmatch / internal/modules/ai/orchestrator.go | ai-intelligence-layer | 5d5ddff70393a197c5f54a45280bb5dd42b24755 | Open original |
| Janady13/missionmatch / internal/modules/ai/orchestrator.go | w3-ai-staffing-recommendations | 43d56387270ef4da4cabd794d97ca6a49909bc7d | Open original |
Recovered source transcript
Download complete source text ↓164 lines · 6.8 KB
package ai
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
"github.com/missionworkforce/missionmatch/internal/platform/audit"
"github.com/missionworkforce/missionmatch/internal/platform/inference"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// Route is the strategy the router picks for a question. The research recommends choosing the cheapest
// sufficient strategy per query (adaptive RAG), and routing aggregation questions to the database rather
// than to vector RAG (docs/AI_RESEARCH.md, routing + TAG).
type Route string
const (
RouteRefuse Route = "refuse" // unsafe input (eeo, injection)
RouteTAG Route = "tag" // the database must compute (count, sum, aggregate)
RouteRAG Route = "rag" // retrieve the tenant's candidates and narrate
RouteDirect Route = "direct" // answer from stable rules in context (CAG), no retrieval
)
// Response is an answer plus the provenance the self correction gate and the audit need.
type Response struct {
Answer string `json:"answer"`
Route Route `json:"route"`
Faithful bool `json:"faithful"`
FaithScore float64 `json:"faithfulness_score"`
EvidenceCount int `json:"evidence_count"`
Refused bool `json:"refused"`
RefusalReason string `json:"refusal_reason,omitempty"`
}
// Orchestrator routes a question across the augmented generation strategies, runs the faithfulness gate
// before any answer ships, and audits every action. It holds the tenant database (RLS scoped) and the
// stateless inference client; it stores nothing itself.
type Orchestrator struct {
db *tenant.DB
client *inference.Client
aud *audit.Writer
}
// NewOrchestrator builds the orchestrator. aud may be nil in tests.
func NewOrchestrator(db *tenant.DB, client *inference.Client, aud *audit.Writer) *Orchestrator {
return &Orchestrator{db: db, client: client, aud: aud}
}
// Classify is the lightweight router. A trained classifier is the documented upgrade; rule based routing
// over question shape is competitive at this scale and is transparent (docs/AI_RESEARCH.md, routing).
func Classify(question string) Route {
q := strings.ToLower(question)
if ForbidEEO(q) != nil {
return RouteRefuse
}
for _, kw := range []string{"how many", "count", "number of", "total", "sum", "average", "percentage", "ratio", "mix", "breakdown"} {
if strings.Contains(q, kw) {
return RouteTAG
}
}
for _, kw := range []string{"who", "which candidate", "find", "show me", "list", "top ", "best ", "match", "search"} {
if strings.Contains(q, kw) {
return RouteRAG
}
}
return RouteDirect
}
// AnswerRAG retrieves the tenant's candidates for the question, narrates strictly from that evidence, and
// runs the faithfulness gate before returning. The deterministic engine remains the floor: this narrates
// over retrieved rows, it never scores.
func (o *Orchestrator) AnswerRAG(ctx context.Context, question string, limit int) (*Response, error) {
if err := ForbidEEO(question); err != nil {
return o.refuse(ctx, RouteRAG, "the question references prohibited demographic data"), nil
}
hits, err := HybridSearch(ctx, o.db, o.client, question, limit)
if err != nil {
return nil, err
}
evidence := make([]string, len(hits))
for i, h := range hits {
evidence[i] = fmt.Sprintf("%s (relevance %.4f)", h.FullName, h.Score)
}
answer, err := o.generate(ctx,
"Answer the question using ONLY the listed candidates as evidence. Cite candidates by name. If the evidence does not answer the question, say so.",
"QUESTION: "+question+"\n\nCANDIDATES:\n"+strings.Join(evidence, "\n"))
if err != nil {
return nil, err
}
return o.gateAndAudit(ctx, RouteRAG, answer, evidence)
}
// AnswerTAG runs a curated, parameterized, read only intent and narrates the database's own result. This
// is the path for aggregation questions; the model never authors SQL.
func (o *Orchestrator) AnswerTAG(ctx context.Context, intentName string, args []any) (*Response, error) {
intent, ok := IntentByName(intentName)
if !ok {
return nil, fmt.Errorf("ai: unknown intent %q", intentName)
}
res, err := ExecuteIntent(ctx, o.db, intent, args)
if err != nil {
return nil, err
}
rendered, _ := json.Marshal(map[string]any{"columns": res.Columns, "rows": res.Rows})
evidence := []string{string(rendered)}
answer, err := o.generate(ctx,
"State the result plainly from the table data. Do not invent numbers; report exactly what the rows say.",
"INTENT: "+intent.Description+"\n\nRESULT:\n"+string(rendered))
if err != nil {
return nil, err
}
return o.gateAndAudit(ctx, RouteTAG, answer, evidence)
}
// generate calls the plane with the injection guard already wrapped on the gateway side.
func (o *Orchestrator) generate(ctx context.Context, system, prompt string) (string, error) {
out, err := o.client.Generate(ctx, inference.GenerateRequest{System: system, Prompt: prompt, MaxTokens: 400, Temperature: 0.1})
if err != nil {
return "", err
}
return strings.TrimSpace(out.Text), nil
}
// gateAndAudit runs the faithfulness check (the self correction gate) and writes the AEDT audit event.
// An unfaithful answer is NOT shipped as a confident answer: it is flagged, and the caller can refuse or
// route to a human. The audit records the route and the verdict, never the answer text or any candidate.
func (o *Orchestrator) gateAndAudit(ctx context.Context, route Route, answer string, evidence []string) (*Response, error) {
resp := &Response{Answer: answer, Route: route, EvidenceCount: len(evidence)}
verdict, err := o.client.CheckFaithfulness(ctx, answer, evidence)
if err == nil {
resp.Faithful = verdict.Supported
resp.FaithScore = verdict.Score
}
if !resp.Faithful {
// Fail safe: an answer that the gate cannot confirm is supported is withheld as an answer.
resp.Refused = true
resp.RefusalReason = "the answer was not confirmed faithful to the evidence; routed for human review"
resp.Answer = ""
}
o.audit(ctx, "aedt.ai.answered", map[string]any{"route": route, "faithful": resp.Faithful, "evidence": len(evidence)})
return resp, nil
}
func (o *Orchestrator) refuse(ctx context.Context, route Route, reason string) *Response {
o.audit(ctx, "aedt.ai.refused", map[string]any{"route": route, "reason": reason})
return &Response{Route: route, Refused: true, RefusalReason: reason}
}
// audit writes one append only AEDT event for an AI action. Payloads carry the route and verdict only,
// never the answer text, the question, or any candidate, so the audit itself holds no PII.
func (o *Orchestrator) audit(ctx context.Context, action string, payload map[string]any) {
if o.aud == nil || o.db == nil {
return
}
body, _ := json.Marshal(payload)
_ = o.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
return o.aud.OnTx(ctx, tx, audit.Event{Action: action, EntityType: "ai", NewValues: body})
})
}