package cognition import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/andydixon/noema/internal/beliefs" "github.com/andydixon/noema/internal/brain" "github.com/andydixon/noema/internal/conversation" "github.com/andydixon/noema/internal/epistemics" evstore "github.com/andydixon/noema/internal/events" "github.com/andydixon/noema/internal/llm" "github.com/andydixon/noema/internal/memory" "github.com/andydixon/noema/internal/workspace" "github.com/andydixon/noema/pkg/concepts" "github.com/andydixon/noema/pkg/events" "github.com/andydixon/noema/pkg/ids" ) // Intent is the structured decision of what to say, built from state before // any language is generated. It is stored with the reply so the operator can // check that the words match the intent and the intent matches the evidence. type Intent struct { Kind string `json:"kind"` // answer | introspect | acknowledge | clarify | decline | greet | unknown Summary string `json:"summary"` Points []string `json:"points"` Beliefs []BeliefRef `json:"beliefs"` Memories []MemRef `json:"memories"` Uncertainty string `json:"uncertainty,omitempty"` Limitations []string `json:"limitations,omitempty"` Analysis any `json:"analysis"` } // BeliefRef cites a belief. type BeliefRef struct { ID ids.ID `json:"id"` Proposition string `json:"proposition"` Confidence float64 `json:"confidence"` Status string `json:"status"` } // MemRef cites a memory. type MemRef struct { ID ids.ID `json:"id"` Kind string `json:"kind"` Content string `json:"content"` Confidence float64 `json:"confidence"` } // perceive is the handler for admitted "perceive" thoughts: it records the // perception, encodes the episode, turns claims into beliefs, builds the // response intent and generates the reply. func (h Handlers) perceive(ctx context.Context, c *brain.Cycle, t workspace.Thought) error { raw, _ := json.Marshal(t.Content["analysis"]) var a conversation.Analysis json.Unmarshal(raw, &a) //nolint:errcheck text, _ := t.Content["text"].(string) actor, _ := t.Content["actor"].(string) convID, _ := t.Content["conversation_id"].(string) msgID, _ := t.Content["message_id"].(string) srcID, _ := t.Content["source_event"].(string) trust := events.Trust(fmt.Sprint(t.Content["trust"])) if !trust.Valid() { trust = events.TrustUser } mind := c.State.Mind if call, ok := t.Content["llm_call"].(string); ok && call != "" { c.Emit(events.New{Type: events.LLMResponse, Source: "brain.perceive", Summary: "perception enriched by " + fmt.Sprint(t.Content["llm_model"]), Salience: 0.1, Confidence: 0.8, CausationID: t.EventID, Evidence: []events.EvidenceRef{{Kind: "llm_call", ID: call}}, Metadata: map[string]any{"role": "perceive", "call_id": call}}) } perc, err := c.EmitNow(ctx, events.New{Type: events.Perception, Source: "brain.perceive", Summary: fmt.Sprintf("perceived %s from %s: %s", a.Intent, actor, truncate(text, 100)), Salience: t.Score, Confidence: 0.9, CausationID: t.EventID, ParentIDs: []ids.ID{t.EventID, ids.ID(srcID)}, Evidence: t.Evidence, Metadata: map[string]any{"intent": a.Intent, "introspection": a.Introspection, "conversation_id": convID, "message_id": msgID, "entities": a.Entities}, Payload: map[string]any{"analysis": a, "text": text, "actor": actor}}) if err != nil { return err } // Episode of the exchange. var memRef ids.ID if h.Memory != nil { mem, err := h.Memory.Create(ctx, c.Tx, memory.New{MindID: mind.ID, Kind: concepts.Episodic, Epistemic: concepts.Observation, Content: actor + " said: " + text, Source: "conversation", Trust: trust, Confidence: 1, Importance: 0.4 + 0.3*a.Urgency, Salience: t.Score, OccurredAt: c.State.Now, OriginEvent: ids.ID(srcID), Detail: map[string]any{"participants": []string{actor}, "channel": "conversation", "conversation_id": convID, "intent": a.Intent, "entities": a.Entities}}) if err == nil { memRef = mem.ID c.Emit(events.New{Type: events.MemoryCreated, Source: "brain.memory", Summary: "remembered: " + truncate(mem.Content, 100), Salience: 0.3, Confidence: 1, CausationID: perc.ID, Evidence: []events.EvidenceRef{{Kind: "memory", ID: string(mem.ID)}, {Kind: "event", ID: srcID, Note: "source"}}, Metadata: map[string]any{"memory_id": mem.ID, "kind": mem.Kind}}) } } // Every claim is first an assertion: the certain fact that someone took a // stance. Only factual assertions (direct or reported) then feed a // proposition belief, as one piece of evidence attributed to whoever // actually stands behind it; hypotheticals, questions and imperatives // never do, and expressed doubt counts against. var claimed []BeliefRef var asserted []AssertionRef if h.Beliefs != nil { for _, cl := range a.Claims { in := epistemics.Interpret(actor, cl.Proposition) if cl.Certainty == "hedged" && in.Certainty == epistemics.CertaintyAsserted { in.Certainty = epistemics.CertaintyHedged } var beliefID ids.ID var res beliefs.Result if in.CreatesBelief() { w := in.EvidenceWeight() if a.Intent == "correction" && !in.Reported { w = min(1, w+0.2) } attributed := in.AttributedTo cred := trust.Credibility() if in.Reported { cred = events.TrustLow.Credibility() + 0.2 // second-hand through a trusted speaker } ev := []beliefs.EvidenceInput{{Direction: "for", Kind: "assertion", RefID: "", Source: "asserted-by:" + attributed, Weight: w, Credibility: cred, Note: describeAssertion(actor, in), Derivation: beliefs.DerivationAssertion, Family: beliefs.FamilyForActor(attributed), RootIDs: []string{srcID}}} if dw := in.DoubtWeight(); dw > 0 { ev = append(ev, beliefs.EvidenceInput{Direction: "against", Kind: "assertion", Source: "doubted-by:" + actor, Weight: dw, Credibility: trust.Credibility(), Note: actor + " doubts it", Derivation: beliefs.DerivationAssertion, Family: beliefs.FamilyForActor(actor), RootIDs: []string{srcID}}) } var err error observed := c.State.Now res, err = h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: mind.ID, Proposition: in.Proposition, Epistemic: concepts.Claim, Source: "conversation", OriginEvent: perc.ID, Causation: perc.ID, Correlation: c.State.CycleID, Evidence: ev, ObservedAt: &observed, Detail: map[string]any{"attributed_to": attributed, "reported": in.Reported, "interpretation_confidence": in.Confidence}}) if err != nil { c.Emit(events.New{Type: events.Metacognition, Source: "brain.perceive", Summary: "claim not recorded: " + err.Error(), Salience: 0.3, Confidence: 1, Metadata: map[string]any{"check": "claim_rejected"}}) } else { beliefID = res.Belief.ID claimed = append(claimed, BeliefRef{ID: res.Belief.ID, Proposition: res.Belief.Proposition, Confidence: res.Belief.Confidence, Status: string(res.Belief.Status)}) } } if h.Assertions != nil { rec, err := h.Assertions.Record(ctx, c.Tx, epistemics.New{MindID: mind.ID, Actor: actor, ActorKind: string(trust), Interpretation: in, Subject: subjectKey(in.Proposition), SourceEvent: ids.ID(srcID), ConversationID: ids.ID(convID), MessageID: ids.ID(msgID), Context: truncate(text, 300), Trust: string(trust), BeliefID: beliefID}) if err != nil { return err } asserted = append(asserted, AssertionRef{ID: rec.ID, AttributedTo: rec.AttributedTo, Proposition: rec.Proposition, Stance: rec.Stance, Modality: rec.Modality, Reported: rec.Reported, Hypothetical: rec.Hypothetical, Interpretation: rec.Interpretation, BeliefID: beliefID}) } } } _ = asserted refs := map[string]ids.ID{"memory": memRef} for _, b := range claimed { refs["belief"] = b.ID } h.linkEntities(ctx, c, a.Entities, actor, ids.ID(srcID), refs) intent := h.buildIntent(ctx, c, a, text, claimed) reply, callID, err := h.generate(ctx, c, text, intent) if err != nil { return err } evidence := []events.EvidenceRef{{Kind: "event", ID: srcID, Note: "question"}, {Kind: "event", ID: string(perc.ID), Note: "perception"}} for _, b := range intent.Beliefs { evidence = append(evidence, events.EvidenceRef{Kind: "belief", ID: string(b.ID), Note: fmt.Sprintf("cited %.2f", b.Confidence)}) } for _, m := range intent.Memories { evidence = append(evidence, events.EvidenceRef{Kind: "memory", ID: string(m.ID), Note: "cited"}) } if !memRef.IsZero() { evidence = append(evidence, events.EvidenceRef{Kind: "memory", ID: string(memRef), Note: "episode"}) } if !callID.IsZero() { evidence = append(evidence, events.EvidenceRef{Kind: "llm_call", ID: string(callID), Note: "wording"}) } respEv, err := c.EmitNow(ctx, events.New{Type: events.ResponseGenerated, Source: "brain.respond", Summary: "replied (" + intent.Kind + "): " + truncate(reply, 120), Salience: 0.5, Confidence: intentConfidence(intent), CausationID: perc.ID, ParentIDs: []ids.ID{perc.ID}, Evidence: evidence, Metadata: map[string]any{"conversation_id": convID, "message_id": msgID, "intent": intent.Kind, "llm_call": callID}, Payload: map[string]any{"intent": intent, "reply": reply, "model_used": !callID.IsZero()}}) if err != nil { return err } if h.Conversations != nil && convID != "" { intentJSON, _ := json.Marshal(intent) if _, err := h.Conversations.AddMessage(ctx, c.Tx, conversation.Message{ConversationID: ids.ID(convID), MindID: mind.ID, Role: "mind", Actor: mind.DisplayName, Content: reply, EventID: respEv.ID, ReplyTo: ids.ID(msgID), Intent: intentJSON, LLMCallID: callID}); err != nil { return fmt.Errorf("store reply: %w", err) } } c.Runtime.Hold(workspace.Item{Key: "conversation:" + convID, Content: actor + ": " + truncate(text, 80) + " → " + truncate(reply, 80), Kind: "thought", SourceEvent: ids.ID(srcID), Ref: convID}) return nil } func intentConfidence(in Intent) float64 { var max float64 for _, b := range in.Beliefs { if b.Confidence > max { max = b.Confidence } } switch in.Kind { case "unknown", "decline": return 0.9 // confident that it does not know case "introspect", "acknowledge", "greet": return 0.95 } if max == 0 { return 0.5 } return max } // buildIntent decides what to convey from state alone. func (h Handlers) buildIntent(ctx context.Context, c *brain.Cycle, a conversation.Analysis, text string, claimed []BeliefRef) Intent { mind := c.State.Mind in := Intent{Analysis: a, Limitations: c.State.SelfModel.Limitations} if len(in.Limitations) > 3 { in.Limitations = in.Limitations[:3] } topic := a.Topic if topic == "" { topic = text } switch a.Intent { case "greeting": in.Kind, in.Summary = "greet", "Greet "+firstNonEmpty(a.Topic, "the user")+" briefly and offer to help." case "statement", "correction": in.Kind = "acknowledge" if len(claimed) == 0 { in.Summary = "Acknowledge the message; no checkable claim was found in it." break } in.Beliefs = claimed in.Summary = fmt.Sprintf("Acknowledge that %d statement(s) were recorded as claims with the shown confidence, noting they rest on this message alone.", len(claimed)) for _, b := range claimed { in.Points = append(in.Points, fmt.Sprintf("Recorded \"%s\" as a %s at confidence %.2f.", b.Proposition, b.Status, b.Confidence)) if b.Status == string(beliefs.StatusContested) { in.Points = append(in.Points, "That conflicts with something I already held; both are now contested.") } } case "request": in.Kind = "decline" var usable []string for _, cap := range c.State.SelfModel.Capabilities { if cap.Policy == "allow" || cap.Policy == "ask" { usable = append(usable, cap.Name+" ("+cap.Policy+")") } } if len(usable) == 0 { in.Summary = "Explain honestly that no external capabilities are granted, so the request cannot be carried out; offer what is known instead." in.Points = append(in.Points, "I have no capabilities granted to act on external systems.") } else { in.Kind = "acknowledge" in.Summary = "Say which capabilities could apply and that any action goes through policy; if one was proposed this cycle its status is in working memory." in.Points = append(in.Points, "Capabilities I may use: "+strings.Join(usable, ", ")+".") for _, it := range c.Runtime.Workspace().WorkingMemory { if strings.HasPrefix(it.Key, "action:") { in.Points = append(in.Points, "Action status: "+it.Content) } } } h.addKnowledge(ctx, c, &in, topic) case "introspection": in.Kind = "introspect" h.introspect(ctx, c, &in, a, topic) default: // question, other in.Kind = "answer" h.addKnowledge(ctx, c, &in, topic) if len(in.Beliefs) == 0 && len(in.Memories) == 0 { in.Kind = "unknown" in.Summary = "Say plainly that nothing is known about this; do not guess." in.Uncertainty = "No beliefs or memories match the question." } else { in.Summary = "Answer from the cited beliefs and memories, stating confidence; say what is uncertain." } } if in.Points == nil { in.Points = []string{} } if in.Beliefs == nil { in.Beliefs = []BeliefRef{} } if in.Memories == nil { in.Memories = []MemRef{} } _ = mind return in } // addKnowledge cites the beliefs and memories relevant to a topic. func (h Handlers) addKnowledge(ctx context.Context, c *brain.Cycle, in *Intent, topic string) { mind := c.State.Mind.ID if h.Beliefs != nil { if bs, _ := h.Beliefs.Search(ctx, mind, topic, 5); len(bs) > 0 { var maxConf float64 for _, b := range bs { if b.Status == beliefs.StatusRejected { continue } if b.Status == beliefs.StatusUnknown { // Recorded but unsupported either way: say so rather than quote the prior. in.Points = append(in.Points, "I do not know whether "+b.Proposition+": it was recorded, but nothing live supports or opposes it.") continue } in.Beliefs = append(in.Beliefs, BeliefRef{ID: b.ID, Proposition: b.Proposition, Confidence: b.Confidence, Status: string(b.Status)}) in.Points = append(in.Points, fmt.Sprintf("%s (%s, %.2f)", b.Proposition, b.Status, b.Confidence)) if b.Confidence > maxConf { maxConf = b.Confidence } } if maxConf > 0 && maxConf < 0.6 { in.Uncertainty = fmt.Sprintf("The strongest relevant belief is only at %.2f.", maxConf) } } // Historical truth is not overwritten by the present: say what was // true and when, alongside what is believed now. if past, _ := h.Beliefs.History(ctx, mind, topic, 3); len(past) > 0 { for _, b := range past { from := "earlier" if b.ValidFrom != nil { from = b.ValidFrom.Format("15:04 on 2 Jan") } in.Points = append(in.Points, fmt.Sprintf("Earlier: %s (true from %s until %s, then superseded).", b.Proposition, from, b.ValidUntil.Format("15:04 on 2 Jan"))) } } } if h.Memory != nil { if hits, _ := h.Memory.Retrieve(ctx, mind, memory.Query{Text: topic, Limit: 4, Now: c.State.Now, Semantic: true}); len(hits) > 0 { for _, r := range hits { if r.Score < 0.3 { continue } in.Memories = append(in.Memories, MemRef{ID: r.Memory.ID, Kind: string(r.Memory.Kind), Content: truncate(r.Memory.Content, 200), Confidence: r.Memory.Confidence}) in.Points = append(in.Points, fmt.Sprintf("I remember: %s (%s, %.2f)", truncate(r.Memory.Content, 160), r.Memory.Kind, r.Memory.Confidence)) } } } } // introspect answers questions about the mind's own state from that state. func (h Handlers) introspect(ctx context.Context, c *brain.Cycle, in *Intent, a conversation.Analysis, topic string) { mind := c.State.Mind.ID ws := c.Runtime.Workspace() switch a.Introspection { case "thinking": in.Summary = "Describe what is currently in the workspace and working memory, most salient first." for i, t := range ws.Workspace { if i >= 5 { break } in.Points = append(in.Points, fmt.Sprintf("%s (score %.2f)", t.Summary, t.Score)) } for i, it := range ws.WorkingMemory { if i >= 4 { break } in.Points = append(in.Points, "holding: "+truncate(it.Content, 100)) } if len(in.Points) == 0 { in.Points = append(in.Points, "The workspace is empty; nothing is being attended to right now.") } case "beliefs": in.Summary = "List what is believed about the topic with confidence and status; say if nothing is known." h.addKnowledge(ctx, c, in, topic) if len(in.Beliefs) == 0 && len(in.Memories) == 0 { in.Kind, in.Uncertainty = "unknown", "No beliefs or memories concern "+topic+"." } case "why": in.Summary = "Explain the evidence for and against the belief the user asked about, and what would change it." if h.Beliefs != nil { if bs, _ := h.Beliefs.Search(ctx, mind, topic, 1); len(bs) > 0 { b := bs[0] in.Beliefs = append(in.Beliefs, BeliefRef{ID: b.ID, Proposition: b.Proposition, Confidence: b.Confidence, Status: string(b.Status)}) rows, _ := h.Beliefs.EvidenceFor(ctx, c.Tx, mind, b.ID) for _, e := range rows { if e.Retracted { continue } in.Points = append(in.Points, fmt.Sprintf("%s: %s (weight %.2f × credibility %.2f)", e.Direction, firstNonEmpty(e.Note, e.Kind), e.Weight, e.Credibility)) } in.Points = append(in.Points, "Independent evidence from another source would move it most; a contradicting observation would lower it.") } else { in.Kind, in.Summary = "unknown", "Say that no belief matches what was asked." } } case "uncertain": in.Summary = "List the beliefs that are least settled, with confidence." if h.Beliefs != nil { bs, _ := h.Beliefs.Uncertain(ctx, mind, 6) for _, b := range bs { in.Beliefs = append(in.Beliefs, BeliefRef{ID: b.ID, Proposition: b.Proposition, Confidence: b.Confidence, Status: string(b.Status)}) in.Points = append(in.Points, fmt.Sprintf("%s (%s, %.2f)", b.Proposition, b.Status, b.Confidence)) } } if len(in.Points) == 0 { in.Points = append(in.Points, "No beliefs sit in the uncertain band right now.") } case "contradictions": in.Summary = "List contested beliefs and what they conflict with." if h.Beliefs != nil { bs, _ := h.Beliefs.Contested(ctx, mind, 6) for _, b := range bs { in.Beliefs = append(in.Beliefs, BeliefRef{ID: b.ID, Proposition: b.Proposition, Confidence: b.Confidence, Status: string(b.Status)}) links, _ := h.Beliefs.Links(ctx, mind, b.ID) for _, l := range links { if l.Relation == "contradicts" && l.Other != nil { in.Points = append(in.Points, fmt.Sprintf("\"%s\" (%.2f) contradicts \"%s\" (%.2f)", b.Proposition, b.Confidence, l.Other.Proposition, l.Other.Confidence)) } } } } if len(in.Points) == 0 { in.Points = append(in.Points, "No contradictions are currently open.") } case "learned": in.Summary = "Summarise what was recorded in the last seven days: new memories and beliefs." evs, _ := c.Runtime.Events().Query(ctx, evstore.Filter{MindID: mind, Types: []events.Type{events.MemoryCreated, events.BeliefCreated, events.HypothesisCreated, events.MemoryConsolidated}, Since: c.State.Now.Add(-7 * 24 * time.Hour), Limit: 8}) for _, e := range evs { in.Points = append(in.Points, e.Summary) } if len(in.Points) == 0 { in.Points = append(in.Points, "Nothing new was recorded this week.") } case "changed": in.Summary = "List beliefs whose status changed recently and why." if h.Beliefs != nil { ch, _ := h.Beliefs.RecentChanges(ctx, mind, c.State.Now.Add(-30*24*time.Hour), 6) for _, x := range ch { in.Beliefs = append(in.Beliefs, BeliefRef{ID: x.Belief.ID, Proposition: x.Belief.Proposition, Confidence: x.Belief.Confidence, Status: string(x.Belief.Status)}) in.Points = append(in.Points, fmt.Sprintf("\"%s\": %s (%.2f) → %s (%.2f) because %s", x.Belief.Proposition, x.FromStatus, x.FromConf, x.Belief.Status, x.Belief.Confidence, firstNonEmpty(x.Reason, "evidence changed"))) } } if len(in.Points) == 0 { in.Points = append(in.Points, "No belief has changed status in the last month.") } default: // predictions, curious, surprised, patterns: subsystems not yet present in.Kind = "decline" in.Summary = "Explain that this aspect of self-observation is not available in this version, without inventing anything." in.Points = append(in.Points, "I do not yet keep "+a.Introspection+" records, so I cannot report on them honestly.") } } // generate phrases the intent. With a model: the respond prompt, constrained // to cited material. Without: a deterministic rendering of the points. func (h Handlers) generate(ctx context.Context, c *brain.Cycle, text string, in Intent) (string, ids.ID, error) { s := c.State if s.Services.LLM != nil && s.Services.Prompts != nil && s.Services.LLM.Available(ctx, llm.RoleRespond) && s.AllowLLM() { if pr, err := s.Services.Prompts.Get(ctx, "respond"); err == nil { data := map[string]any{"Brief": s.Mind.Personality.Brief(), "Intent": in.Summary + "\n" + strings.Join(in.Points, "\n"), "Beliefs": in.Beliefs, "Memories": in.Memories, "Uncertainty": in.Uncertainty, "Limitations": in.Limitations, "Message": text} if req, err := pr.Render(data); err == nil { req.MindID, req.CorrelationID, req.Purpose = s.Mind.ID, s.CycleID, "respond" c.Emit(events.New{Type: events.LLMRequest, Source: "brain.respond", Summary: "asked model to phrase reply (" + in.Kind + ")", Salience: 0.1, Confidence: 1, Metadata: map[string]any{"role": "respond", "prompt": pr.Name, "version": pr.CurrentVersion}}) resp, err := s.Services.LLM.Complete(ctx, req) if err == nil && strings.TrimSpace(resp.Text) != "" { c.Emit(events.New{Type: events.LLMResponse, Source: "brain.respond", Summary: "model phrased reply via " + resp.Provider + "/" + resp.Model, Salience: 0.1, Confidence: 1, Evidence: []events.EvidenceRef{{Kind: "llm_call", ID: string(resp.CallID)}}, Metadata: map[string]any{"role": "respond", "call_id": string(resp.CallID), "tokens": resp.Usage.TotalTokens}}) return strings.TrimSpace(resp.Text), resp.CallID, nil } c.Emit(events.New{Type: events.Metacognition, Source: "brain.respond", Summary: "model reply failed; using deterministic wording", Salience: 0.3, Confidence: 1, Metadata: map[string]any{"check": "llm_failure", "error": errString(err)}}) } } } return deterministicReply(in), ids.Nil, nil } func errString(err error) string { if err == nil { return "empty response" } return err.Error() } // deterministicReply renders the intent without a model: honest, plain, with // confidences visible. func deterministicReply(in Intent) string { var b strings.Builder switch in.Kind { case "greet": b.WriteString("Hello. I can tell you what I currently hold and how sure I am of it.") case "unknown": b.WriteString("I don't know. Nothing I hold bears on that.") case "decline": b.WriteString("I can't do that.") case "acknowledge": b.WriteString("Noted.") case "introspect": b.WriteString("From my current state:") default: b.WriteString("Here is what I hold on that:") } for _, p := range in.Points { b.WriteString("\n- " + p) } if in.Uncertainty != "" { b.WriteString("\n" + in.Uncertainty) } b.WriteString("\n(No language model is configured, so this is a direct rendering of my state.)") return b.String() } func firstNonEmpty(a, b string) string { if a != "" { return a } return b } // AssertionRef is the response intent's view of a recorded assertion. type AssertionRef struct { ID ids.ID `json:"id"` AttributedTo string `json:"attributed_to"` Proposition string `json:"proposition"` Stance string `json:"stance"` Modality string `json:"modality"` Reported bool `json:"reported"` Hypothetical bool `json:"hypothetical"` Interpretation float64 `json:"interpretation_confidence"` BeliefID ids.ID `json:"belief_id,omitempty"` } // describeAssertion words the evidence note so the belief page says who // stood behind the proposition and how the mind read them. func describeAssertion(speaker string, in epistemics.Interpretation) string { switch { case in.Reported && in.Quoted: return speaker + " quoted " + in.AttributedTo + " (" + in.Certainty + ")" case in.Reported: return speaker + " reported that " + in.AttributedTo + " holds this (" + in.Certainty + ", read with " + fmt.Sprintf("%.0f%%", in.Confidence*100) + " interpretation confidence)" default: return speaker + " " + in.Certainty + " it (read with " + fmt.Sprintf("%.0f%%", in.Confidence*100) + " interpretation confidence)" } }