A statement made to a mind is now three things kept apart: the observation that it arrived, an append-only assertion (attributed actor, stance, certainty, modality, negation, reported or quoted speech, hypothetical status, interpretation confidence and notes), and, only for factual assertions, one piece of evidence for a proposition belief attributed to whoever actually stands behind it. Reported speech is attributed to the person reported and weighed as second-hand; hypotheticals, questions and imperatives create no belief; expressed doubt counts against; a possibly sarcastic remark is recorded with low interpretation confidence rather than guessed at. Interpretation is deterministic code; a model may enrich the claim list but never sets stance or confidence. Association no longer treats conversational turns as world events: hypotheticals, questions, requests and consecutive turns by one speaker cannot seed causal hypotheses. Adds internal/epistemics, migration 0018, the assertions API, assertions on belief pages and in belief detail, a shared end-to-end test harness (tests/harness), end-to-end tests for eight speech patterns, and updates the milestone test to the new provenance.
223 lines
10 KiB
Go
223 lines
10 KiB
Go
// Package epistemics keeps apart three things a careless system conflates:
|
|
// the observation that somebody said something, the assertion they made,
|
|
// and any belief about whether what they said is true.
|
|
//
|
|
// An Assertion is the durable, append-only record that an actor took a
|
|
// stance on a proposition. Its confidence is about interpretation ("how sure
|
|
// are we that Andy asserted X?"), never about X. Whether X is believed is a
|
|
// separate question answered by the belief layer from evidence, of which the
|
|
// assertion is one row with a weight that depends on stance, modality and
|
|
// how clearly it was understood. Reported speech attributes the stance to
|
|
// the person reported, hypotheticals create no belief at all, doubt counts
|
|
// against, and a possibly sarcastic remark is recorded with low
|
|
// interpretation confidence rather than guessed at.
|
|
package epistemics
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Stances an actor can take towards a proposition.
|
|
const (
|
|
StanceAsserts = "asserts"
|
|
StanceDenies = "denies"
|
|
StanceDoubts = "doubts"
|
|
StanceQuestions = "questions"
|
|
StanceSupposes = "supposes"
|
|
StanceReports = "reports" // the speaker relays somebody else's stance
|
|
)
|
|
|
|
// Certainty the speaker expressed.
|
|
const (
|
|
CertaintyAsserted = "asserted"
|
|
CertaintyHedged = "hedged"
|
|
CertaintyUncertain = "uncertain"
|
|
CertaintyEmphatic = "emphatic"
|
|
)
|
|
|
|
// Modality of the utterance.
|
|
const (
|
|
ModalityFactual = "factual"
|
|
ModalityHypothetical = "hypothetical"
|
|
ModalityReported = "reported"
|
|
ModalityQuoted = "quoted"
|
|
ModalityImperative = "imperative"
|
|
ModalityQuestion = "question"
|
|
)
|
|
|
|
// Interpretation is the deterministic reading of one claim sentence.
|
|
type Interpretation struct {
|
|
Proposition string `json:"proposition"` // the proposition as attributed (reporting frame removed)
|
|
AttributedTo string `json:"attributed_to"` // whose stance this is
|
|
Stance string `json:"stance"` // stance of AttributedTo towards Proposition
|
|
SpeakerStance string `json:"speaker_stance"` // stance of the speaker when they report somebody else ("" = same)
|
|
Certainty string `json:"certainty"`
|
|
Modality string `json:"modality"`
|
|
Negated bool `json:"negated"`
|
|
Reported bool `json:"reported"`
|
|
ReportedActor string `json:"reported_actor,omitempty"`
|
|
Quoted bool `json:"quoted"`
|
|
Hypothetical bool `json:"hypothetical"`
|
|
Confidence float64 `json:"interpretation_confidence"` // how sure the reading itself is
|
|
Notes []string `json:"notes,omitempty"`
|
|
}
|
|
|
|
var (
|
|
reportVerbs = `(?:says?|said|saying|thinks?|thought|thinking|believes?|believed|claims?|claimed|claiming|told (?:me|us)|telling (?:me|us)|reckons?|insists?|insisting|mentioned|reported|argues?|arguing|swears?|maintains?)`
|
|
reReported = regexp.MustCompile(`(?i)^(?:apparently[, ]+|well[, ]+)?([A-Z][\w'.-]*(?: [A-Z][\w'.-]*)?)\s+` + reportVerbs + `\s+(?:that\s+)?(.+)$`)
|
|
reQuoted = regexp.MustCompile(`(?i)^([A-Z][\w'.-]*(?: [A-Z][\w'.-]*)?)\s+` + reportVerbs + `[:,]?\s*["“](.+?)["”]\.?$`)
|
|
reDiffuse = regexp.MustCompile(`(?i)^(?:people|everyone|everybody|they|folks|some people|others|the team|the internet)\s+(?:keep |keeps |always |all )?` + reportVerbs + `\s+(?:that\s+)?(.+)$`)
|
|
reHypothet = regexp.MustCompile(`(?i)^(?:imagine|suppose|supposing|assume|assuming|let'?s say|what if|hypothetically|pretend|say)\b[,:]?\s*(?:that\s+)?(.+)$`)
|
|
reConditional = regexp.MustCompile(`(?i)^if\s+(.+?),\s*(?:then\s+)?(.+)$`)
|
|
reDisagree = regexp.MustCompile(`(?i)^(.+?)[,;]?\s+(?:but|though|although)\s+(?:i|we)\s+(?:don'?t|do not|didn'?t|can'?t|cannot|never)\s+(?:believe|buy|trust|think so|agree)(?:\s+(?:her|him|them|it|that|this))?\.?$`)
|
|
reDoubtSuffix = regexp.MustCompile(`(?i)^(.+?)[,;]?\s+(?:which|and that|but that)\s+(?:is|was|seems)\s+(?:nonsense|rubbish|wrong|false|unlikely|doubtful)\.?$`)
|
|
reNegation = regexp.MustCompile(`(?i)\b(?:not|no|never|isn't|isnt|aren't|wasn't|weren't|doesn't|don't|didn't|cannot|can't|won't|wouldn't|shouldn't|nobody|nothing|none)\b`)
|
|
reEmphatic = regexp.MustCompile(`(?i)\b(?:definitely|certainly|absolutely|without doubt|i'?m (?:sure|certain)|for sure|no doubt|clearly)\b`)
|
|
reHedge = regexp.MustCompile(`(?i)\b(?:i think|i believe|maybe|perhaps|possibly|might|probably|not sure|i guess|seems|i suspect|apparently|likely|sort of|kind of)\b`)
|
|
reSarcasm = regexp.MustCompile(`(?i)^(?:oh (?:yes|yeah|sure|right)|yeah,? right|sure,? and|of course|great,)\b|\b(?:absolute|total|utter|complete)\s+(?:genius|hero|legend|star|expert|joy|delight)\b|\b(?:as always|as usual|what a surprise|shock horror)\b`)
|
|
reImperative = regexp.MustCompile(`(?i)^(?:please\s+)?(?:do|make|run|go|check|tell|show|remember|note|delete|fetch|read|stop|start|send|install)\b`)
|
|
)
|
|
|
|
// Interpret reads one claim sentence spoken by speaker.
|
|
func Interpret(speaker, sentence string) Interpretation {
|
|
s := strings.TrimSpace(sentence)
|
|
s = strings.TrimRight(s, " .!")
|
|
in := Interpretation{Proposition: s, AttributedTo: speaker, Stance: StanceAsserts, Certainty: CertaintyAsserted, Modality: ModalityFactual, Confidence: 0.85}
|
|
lower := strings.ToLower(s)
|
|
if strings.HasSuffix(sentence, "?") {
|
|
in.Stance, in.Modality, in.Confidence = StanceQuestions, ModalityQuestion, 0.9
|
|
return in
|
|
}
|
|
if reImperative.MatchString(s) {
|
|
in.Modality, in.Stance, in.Confidence = ModalityImperative, StanceSupposes, 0.7
|
|
in.Notes = append(in.Notes, "imperative: a request, not a claim about the world")
|
|
return in
|
|
}
|
|
// Hypotheticals and conditionals create no commitment.
|
|
if m := reHypothet.FindStringSubmatch(s); m != nil {
|
|
in.Proposition, in.Hypothetical, in.Modality, in.Stance, in.Confidence = strings.TrimSpace(m[1]), true, ModalityHypothetical, StanceSupposes, 0.85
|
|
in.Notes = append(in.Notes, "hypothetical framing; no commitment to the proposition")
|
|
in.Negated = reNegation.MatchString(in.Proposition)
|
|
return in
|
|
}
|
|
if m := reConditional.FindStringSubmatch(s); m != nil {
|
|
in.Hypothetical, in.Modality, in.Stance, in.Confidence = true, ModalityHypothetical, StanceSupposes, 0.7
|
|
in.Notes = append(in.Notes, "conditional; the consequent is asserted only under the antecedent")
|
|
return in
|
|
}
|
|
// Disagreement with reported speech: "Sarah says X, but I don't believe her".
|
|
if m := reDisagree.FindStringSubmatch(s); m != nil {
|
|
inner := Interpret(speaker, m[1])
|
|
inner.SpeakerStance = StanceDoubts
|
|
inner.Notes = append(inner.Notes, "speaker distances themselves from the reported claim")
|
|
if !inner.Reported {
|
|
// "X, but I don't believe it": the speaker relays a claim without an author.
|
|
inner.Reported, inner.ReportedActor, inner.AttributedTo, inner.Modality = true, "unspecified", "unspecified", ModalityReported
|
|
inner.Confidence = minf(inner.Confidence, 0.6)
|
|
}
|
|
return inner
|
|
}
|
|
if m := reDoubtSuffix.FindStringSubmatch(s); m != nil {
|
|
inner := Interpret(speaker, m[1])
|
|
inner.SpeakerStance = StanceDoubts
|
|
inner.Notes = append(inner.Notes, "speaker rejects the claim")
|
|
return inner
|
|
}
|
|
// Quoted and reported speech attribute the stance to the person quoted.
|
|
if m := reQuoted.FindStringSubmatch(s); m != nil {
|
|
in.ReportedActor, in.Proposition = m[1], strings.TrimSpace(m[2])
|
|
in.Reported, in.Quoted, in.Modality, in.AttributedTo, in.Confidence = true, true, ModalityQuoted, m[1], 0.8
|
|
in.SpeakerStance = StanceReports
|
|
in.Negated = reNegation.MatchString(in.Proposition)
|
|
in.Certainty = certaintyOf(in.Proposition)
|
|
return in
|
|
}
|
|
if m := reDiffuse.FindStringSubmatch(s); m != nil {
|
|
in.ReportedActor, in.Proposition = "unspecified people", strings.TrimSpace(m[1])
|
|
in.Reported, in.Modality, in.AttributedTo, in.Confidence = true, ModalityReported, "unspecified people", 0.6
|
|
in.SpeakerStance, in.Certainty = StanceReports, CertaintyUncertain
|
|
in.Notes = append(in.Notes, "diffuse attribution; nobody in particular stands behind it")
|
|
in.Negated = reNegation.MatchString(in.Proposition)
|
|
return in
|
|
}
|
|
if m := reReported.FindStringSubmatch(s); m != nil && !strings.EqualFold(m[1], "I") {
|
|
in.ReportedActor, in.Proposition = m[1], strings.TrimSpace(m[2])
|
|
in.Reported, in.Modality, in.AttributedTo, in.Confidence = true, ModalityReported, m[1], 0.75
|
|
in.SpeakerStance = StanceReports
|
|
in.Negated = reNegation.MatchString(in.Proposition)
|
|
in.Certainty = certaintyOf(in.Proposition)
|
|
if strings.Contains(strings.ToLower(m[0]), "think") || strings.Contains(strings.ToLower(m[0]), "believe") {
|
|
in.Notes = append(in.Notes, "reports a belief, not an observation")
|
|
}
|
|
return in
|
|
}
|
|
// Plain statement by the speaker.
|
|
in.Negated = reNegation.MatchString(lower)
|
|
in.Certainty = certaintyOf(s)
|
|
if reSarcasm.MatchString(s) {
|
|
in.Confidence = 0.4
|
|
in.Notes = append(in.Notes, "possible sarcasm or irony; literal reading uncertain")
|
|
}
|
|
if in.Negated && in.Stance == StanceAsserts {
|
|
in.Notes = append(in.Notes, "negated proposition asserted as stated")
|
|
}
|
|
return in
|
|
}
|
|
|
|
func certaintyOf(s string) string {
|
|
switch {
|
|
case reEmphatic.MatchString(s):
|
|
return CertaintyEmphatic
|
|
case reHedge.MatchString(s):
|
|
return CertaintyHedged
|
|
}
|
|
return CertaintyAsserted
|
|
}
|
|
|
|
// CreatesBelief reports whether an interpretation should feed a proposition
|
|
// belief at all. Questions, imperatives and hypotheticals never do.
|
|
func (in Interpretation) CreatesBelief() bool {
|
|
switch in.Modality {
|
|
case ModalityQuestion, ModalityImperative, ModalityHypothetical:
|
|
return false
|
|
}
|
|
return in.Stance == StanceAsserts && strings.TrimSpace(in.Proposition) != ""
|
|
}
|
|
|
|
// EvidenceWeight is the weight the assertion contributes as evidence for
|
|
// its proposition, before source credibility: how firmly the attributed
|
|
// actor stood behind it, discounted by how surely the reading itself holds.
|
|
func (in Interpretation) EvidenceWeight() float64 {
|
|
w := 0.5
|
|
switch in.Certainty {
|
|
case CertaintyEmphatic:
|
|
w = 0.55
|
|
case CertaintyHedged:
|
|
w = 0.3
|
|
case CertaintyUncertain:
|
|
w = 0.2
|
|
}
|
|
if in.Reported {
|
|
w *= 0.5 // second-hand: the mind did not hear the attributed actor
|
|
}
|
|
return round(w * in.Confidence)
|
|
}
|
|
|
|
// DoubtWeight is the counter-weight the speaker's expressed doubt adds.
|
|
func (in Interpretation) DoubtWeight() float64 {
|
|
if in.SpeakerStance != StanceDoubts {
|
|
return 0
|
|
}
|
|
return round(0.3 * in.Confidence)
|
|
}
|
|
|
|
func minf(a, b float64) float64 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func round(v float64) float64 { return float64(int(v*1000+0.5)) / 1000 }
|