File size: 7,048 Bytes
2e818da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
from app.schemas.commit_adaptation import InteractionQuote, MemoryProposal
from app.services.commit_candidate_adapters import (
    StudentInteraction,
    candidate_to_project_memory_record,
    proposal_to_memory_candidate,
)
from app.services.memory_candidates import MemoryCandidate, make_candidate_id
from app.rag.project_memory import ProjectMemoryRecord


def _interaction(text: str, actor: str = "student") -> StudentInteraction:
    return StudentInteraction(
        interaction_id="interaction-1",
        timestamp=1_700_000_000.0,
        actor=actor,
        source="chat_turn",
        agent_id="tutor",
        text=text,
    )


def test_adapter_builds_real_strict_memory_candidate():
    interactions = {"interaction-1": _interaction("Do not give me the complete code.")}
    proposal = MemoryProposal(
        proposal_id="p1",
        destination="student",
        kind="workflow_preference",
        statement="Prefers to implement code incrementally rather than receiving complete solutions.",
        interaction_ids=["interaction-1"],
        evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="Do not give me the complete code.")],
        confidence=0.9,
    )

    candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions)

    assert isinstance(candidate, MemoryCandidate)
    assert candidate.candidate_id
    assert candidate.interaction_ids == ["interaction-1"]
    assert candidate.evidence_ids == []


def test_explicit_but_not_global_wording_downgrades_to_project_scope():
    interactions = {"interaction-1": _interaction("Do not give me the complete code.")}
    proposal = MemoryProposal(
        proposal_id="p1",
        destination="student",  # LLM proposed global — not corroborated by wording
        kind="workflow_preference",
        statement="Prefers incremental code guidance.",
        interaction_ids=["interaction-1"],
        evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="Do not give me the complete code.")],
        confidence=0.9,
    )

    candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions)

    assert candidate.destination == "project"
    # attribution reflects whether the STATEMENT was explicit, independent of
    # final destination — an explicit-but-project-scoped note is still more
    # reliably attributed than a merely-inferred one.
    assert candidate.attribution == "explicit_student"


def test_explicit_global_wording_keeps_student_destination():
    interactions = {"interaction-1": _interaction("I generally prefer diagrams over equations.")}
    proposal = MemoryProposal(
        proposal_id="p1",
        destination="student",
        kind="learning_style",
        statement="Generally prefers diagrams over equations.",
        interaction_ids=["interaction-1"],
        evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="I generally prefer diagrams over equations.")],
        confidence=0.9,
    )

    candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions)

    assert candidate.destination == "student"
    assert candidate.attribution == "explicit_student"


def test_quote_from_assistant_actor_is_not_verified():
    interactions = {"interaction-1": _interaction("I generally prefer diagrams over equations.", actor="assistant")}
    proposal = MemoryProposal(
        proposal_id="p1",
        destination="student",
        kind="learning_style",
        statement="Generally prefers diagrams over equations.",
        interaction_ids=["interaction-1"],
        evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="I generally prefer diagrams over equations.")],
        confidence=0.9,
    )

    candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions)

    # No verified quote -> not explicit -> not global -> falls back to inferred profile proposal
    assert candidate.attribution == "idea_observer_profile_proposal"
    assert candidate.interaction_ids == []


def test_quote_not_found_in_interaction_text_is_not_verified():
    interactions = {"interaction-1": _interaction("I like short answers.")}
    proposal = MemoryProposal(
        proposal_id="p1",
        destination="student",
        kind="learning_style",
        statement="Fabricated claim.",
        interaction_ids=["interaction-1"],
        evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="I generally prefer diagrams")],
        confidence=0.9,
    )

    candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions)

    assert candidate.attribution == "idea_observer_profile_proposal"
    assert candidate.interaction_ids == []


def test_quote_normalization_tolerates_smart_quotes_case_and_whitespace():
    interactions = {"interaction-1": _interaction("I GENERALLY  prefer’s diagrams.")}
    proposal = MemoryProposal(
        proposal_id="p1",
        destination="student",
        kind="learning_style",
        statement="Generally prefers diagrams.",
        interaction_ids=["interaction-1"],
        evidence_quotes=[InteractionQuote(interaction_id="interaction-1", quote="i generally prefer's diagrams.")],
        confidence=0.9,
    )

    candidate = proposal_to_memory_candidate(proposal, project_id="project-a", interactions_by_id=interactions)

    assert candidate.interaction_ids == ["interaction-1"]


def test_candidate_to_project_memory_record_builds_real_record():
    candidate = MemoryCandidate(
        candidate_id=make_candidate_id("project", "project-a", "project_observation", "Uses PyTorch for all experiments."),
        destination="project",
        project_id="project-a",
        kind="project_observation",
        statement="Uses PyTorch for all experiments.",
        attribution="idea_observer_interaction",
        confidence=0.8,
        interaction_ids=["interaction-1"],
        evidence_ids=[],
    )

    record = candidate_to_project_memory_record(candidate)

    assert isinstance(record, ProjectMemoryRecord)
    assert record.novelty_key == candidate.recurrence_key
    assert record.memory_id.startswith("pm_")
    assert record.attribution == "idea_observer_interaction"


def test_candidate_to_project_memory_record_rejects_student_destination():
    import pytest

    candidate = MemoryCandidate(
        candidate_id=make_candidate_id("student", "project-a", "preferred_name", "The student prefers to be called Anshuman."),
        destination="student",
        project_id="project-a",
        kind="preferred_name",
        statement="The student prefers to be called Anshuman.",
        attribution="explicit_student",
        confidence=0.99,
        interaction_ids=["interaction-1"],
        evidence_ids=[],
    )

    with pytest.raises(ValueError, match="destination='project'"):
        candidate_to_project_memory_record(candidate)