package staffing import ( "context" "errors" "fmt" "strings" "github.com/missionworkforce/missionmatch/internal/modules/ai" "github.com/missionworkforce/missionmatch/internal/platform/inference" ) // narrate produces the natural language rationale for one recommendation and records its source. It is // the ONLY place a model is used, and the recommendation never depends on it: the model writes prose // grounded strictly in the deterministic evidence, the faithfulness gate confirms the prose asserts // nothing the evidence does not support, and any failure (no plane, circuit open, timeout, or an // unconfirmed answer) falls back to a deterministic template assembled from the same facts. The fallback // reason is recorded in the evidence (never in an audit payload) for later narration tuning. // // No candidate name, email, or any demographic field is ever sent to the plane; the facts are the action, // the engine score and band, skill names, requirement ids, and the computed counterfactual numbers. Every // string is passed through ai.ForbidEEO before it leaves the process. func (s *Service) narrate(ctx context.Context, action Action, ev Evidence) (rationale string, source RationaleSource, fallbackReason string) { facts := evidenceStatements(action, ev) deterministic := strings.Join(facts, " ") if deterministic == "" { deterministic = "Recommended by the deterministic engine." } if s.infer == nil { return deterministic, RationaleDeterministic, "generation_disabled" } system := "You write a one or two sentence staffing rationale for a government contractor. Use ONLY the facts provided, assert nothing not present in them, never mention demographics, and never invent a number." prompt := "Facts:\n" + strings.Join(facts, "\n") + "\nWrite the rationale for the " + string(action) + " recommendation." for _, str := range append([]string{system, prompt}, facts...) { if err := ai.ForbidEEO(str); err != nil { return deterministic, RationaleDeterministic, "eeo_guard" } } gen, err := s.infer.Generate(ctx, inference.GenerateRequest{System: system, Prompt: prompt, MaxTokens: 120, Temperature: 0}) if err != nil { return deterministic, RationaleDeterministic, fallbackReason2(err) } answer := strings.TrimSpace(gen.Text) if answer == "" { return deterministic, RationaleDeterministic, "empty_generation" } // Defense in depth: if the model echoed an eeo reference, refuse the prose. if ai.ForbidEEO(answer) != nil { return deterministic, RationaleDeterministic, "eeo_guard" } fr, err := s.infer.CheckFaithfulness(ctx, answer, facts) if err != nil { return deterministic, RationaleDeterministic, fallbackReason2(err) } if !fr.Supported { return deterministic, RationaleDeterministic, "could_not_confirm" } return answer, RationaleAIFaithful, "" } // evidenceStatements renders the deterministic facts a rationale may assert, from the structured evidence. // These are also the evidence array the faithfulness gate checks the generated prose against. func evidenceStatements(action Action, ev Evidence) []string { var out []string if ev.EngineTotal != "" { band := ev.Band if band == "" { band = "n/a" } out = append(out, "Engine total score is "+ev.EngineTotal+" out of 100, band "+band+".") } switch action { case ActionAssign: out = append(out, "The candidate is available and clears the staffing floor.") case ActionReassign: if ev.Reassign != nil { out = append(out, "The candidate is committed to requirement "+ev.Reassign.FromRequirementID+"; the target gap is higher priority by "+ev.Reassign.PriorityDelta.String()+".") } case ActionReskill: if ev.Reskill != nil { out = append(out, "The candidate holds "+ev.Reskill.HeldSkill+", adjacent to the missing required skill "+ev.Reskill.MissingSkill+" at cosine distance "+ev.Reskill.Distance.String()+".") } case ActionSource: if ev.OpenSlots != nil { out = append(out, fmt.Sprintf("No qualified, available bench candidate exists for %d open slot(s); external sourcing is required.", *ev.OpenSlots)) } } if cf := ev.Counterfactual; cf != nil { out = append(out, "The assigned candidate's engine total for this requirement is "+cf.AssignedTotal+".") if cf.CandidateNextRequirement != "" { out = append(out, "Their next best alternative is requirement "+cf.CandidateNextRequirement+".") } if cf.RequirementNextCandidate != "" { out = append(out, "If reassigned, this requirement's next best candidate is "+cf.RequirementNextCandidate+".") } } if ev.Trigger != nil { out = append(out, "This alert was raised by a "+ev.Trigger.Kind+" change.") } return out } func fallbackReason2(err error) string { switch { case errors.Is(err, inference.ErrCircuitOpen): return "circuit_open" case errors.Is(err, context.DeadlineExceeded): return "plane_timeout" default: return "plane_error" } }