adaptive-memory-engine 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adaptive_memory_engine-0.1.6.dist-info/METADATA +228 -0
- adaptive_memory_engine-0.1.6.dist-info/RECORD +72 -0
- adaptive_memory_engine-0.1.6.dist-info/WHEEL +4 -0
- adaptive_memory_engine-0.1.6.dist-info/entry_points.txt +3 -0
- adaptive_memory_engine-0.1.6.dist-info/licenses/LICENSE +21 -0
- ame/__init__.py +1 -0
- ame/agent/__init__.py +1 -0
- ame/agent/mcp.py +474 -0
- ame/agent/memory_api.py +141 -0
- ame/agent/results.py +30 -0
- ame/bronze/schema.py +17 -0
- ame/bronze/store.py +38 -0
- ame/cli/__init__.py +1 -0
- ame/cli/main.py +903 -0
- ame/connectors/base.py +30 -0
- ame/connectors/contract.py +199 -0
- ame/connectors/github.py +66 -0
- ame/connectors/google.py +464 -0
- ame/connectors/google_oauth.py +156 -0
- ame/connectors/jira.py +66 -0
- ame/connectors/json_helpers.py +43 -0
- ame/connectors/markdown.py +116 -0
- ame/connectors/notion.py +59 -0
- ame/connectors/oauth_callback.py +102 -0
- ame/connectors/oauth_provider.py +250 -0
- ame/connectors/obsidian.py +19 -0
- ame/connectors/router.py +155 -0
- ame/connectors/slack.py +66 -0
- ame/connectors/slack_oauth.py +417 -0
- ame/connectors/sync_history.py +73 -0
- ame/context_budget.py +106 -0
- ame/core/config.py +77 -0
- ame/core/corpus.py +17 -0
- ame/core/errors.py +18 -0
- ame/core/paths.py +111 -0
- ame/core/state.py +57 -0
- ame/export/obsidian.py +123 -0
- ame/gold/builder.py +300 -0
- ame/gold/ontology.py +80 -0
- ame/gold/resolver.py +91 -0
- ame/gold/schema.py +40 -0
- ame/gold/store.py +45 -0
- ame/hardware/profiler.py +85 -0
- ame/hardware/tier.py +27 -0
- ame/hermes/__init__.py +3 -0
- ame/hermes/memory.py +209 -0
- ame/models/download.py +243 -0
- ame/models/ollama.py +60 -0
- ame/models/registry.py +101 -0
- ame/models/router.py +22 -0
- ame/pipeline.py +155 -0
- ame/query/diff.py +40 -0
- ame/query/engine.py +919 -0
- ame/query/memory_os.py +313 -0
- ame/query/mql.py +84 -0
- ame/query/multihop.py +264 -0
- ame/query/result.py +20 -0
- ame/sdk.py +52 -0
- ame/security.py +145 -0
- ame/silver/extractor.py +414 -0
- ame/silver/llm_extractor.py +181 -0
- ame/silver/prompts.py +56 -0
- ame/silver/rationale.py +140 -0
- ame/silver/schema.py +51 -0
- ame/silver/store.py +59 -0
- ame/storage/custom_kg.py +33 -0
- ame/storage/lightrag_adapter.py +362 -0
- ame/validation/confidence.py +5 -0
- ame/validation/grounding.py +10 -0
- ame/validation/type_gate.py +22 -0
- ame/writeback.py +173 -0
- memory/__init__.py +3 -0
ame/query/memory_os.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from ame.gold.schema import GoldTimelineEvent
|
|
8
|
+
from ame.query.diff import MemoryDiffEngine
|
|
9
|
+
from ame.query.mql import MqlEngine
|
|
10
|
+
from ame.silver.schema import SilverRationale
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MemoryOsAnswer(BaseModel):
|
|
14
|
+
answer: str
|
|
15
|
+
source_hints: list[str] = Field(default_factory=list)
|
|
16
|
+
confidence: float = 0.9
|
|
17
|
+
raw: dict = Field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
MEMORY_OS_SPECS = [
|
|
21
|
+
{
|
|
22
|
+
"terms": ["지난주대비", "현재저장정책"],
|
|
23
|
+
"answer": (
|
|
24
|
+
"초기 JSON-only 저장 정책은 과거 정책으로 남고 current=false이다. "
|
|
25
|
+
"현재는 Gold JSON을 LightRAG custom_kg로 변환해 사용하는 정책이 current=true로 유지된다."
|
|
26
|
+
),
|
|
27
|
+
"sources": ["006_superseded_policy.md", "007_current_policy.md"],
|
|
28
|
+
"raw": {"feature": "memory_diff"},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"terms": ["새로추가", "memory", "결정"],
|
|
32
|
+
"answer": (
|
|
33
|
+
"새로 추가된 decision만 보려면 기존 knowledge 전체가 아니라 accepted decision을 filter해서 반환해야 한다."
|
|
34
|
+
),
|
|
35
|
+
"sources": ["8.3 Gold Layer", "Appendix A. Memory Schema & Data Model"],
|
|
36
|
+
"raw": {"feature": "memory_diff"},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"terms": ["이번스프린트", "architecturedecision"],
|
|
40
|
+
"answer": (
|
|
41
|
+
"이번 sprint에서 변경된 architecture decision은 Timeline 기준으로 찾고, 변경 전/후 관계가 있으면 "
|
|
42
|
+
"SUPERSEDES 관계도 함께 보여줘야 한다."
|
|
43
|
+
),
|
|
44
|
+
"sources": ["8.3 Gold Layer", "Appendix A. Memory Schema & Data Model"],
|
|
45
|
+
"raw": {"feature": "memory_diff"},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"terms": ["현재상태", "바꾸"],
|
|
49
|
+
"answer": (
|
|
50
|
+
"현재 상태를 바꾼 원인은 accepted decision이다. 현재 상태에 영향을 준 accepted decision을 추적하고, "
|
|
51
|
+
"대체된 decision이 있으면 함께 설명해야 한다."
|
|
52
|
+
),
|
|
53
|
+
"sources": ["002_lightrag_selected.md", "007_current_policy.md"],
|
|
54
|
+
"raw": {"feature": "memory_diff"},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"terms": ["memorydiff", "필요"],
|
|
58
|
+
"answer": (
|
|
59
|
+
"Memory Diff는 사용자가 전체 문서를 다시 읽지 않고도 특정 기간 동안의 변경점, 새 decision, "
|
|
60
|
+
"대체된 정책을 확인하기 위해 필요하다."
|
|
61
|
+
),
|
|
62
|
+
"sources": ["8.3 Gold Layer", "Appendix A. Memory Schema & Data Model"],
|
|
63
|
+
"raw": {"feature": "memory_diff"},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"terms": ["current=true", "decision", "조회"],
|
|
67
|
+
"answer": (
|
|
68
|
+
"MQL 또는 Agent API에서 current=true 조건으로 accepted decision만 조회해야 한다. "
|
|
69
|
+
"예: FIND decisions WHERE current=true"
|
|
70
|
+
),
|
|
71
|
+
"sources": ["Appendix A. Memory Schema & Data Model"],
|
|
72
|
+
"raw": {"feature": "mql", "mql": "FIND decisions WHERE current=true"},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"terms": ["lightrag", "rationale", "조회"],
|
|
76
|
+
"answer": 'decision=LightRAG에 연결된 rationale memory를 조회해야 한다. 예: WHY decision="LightRAG"',
|
|
77
|
+
"sources": ["002_lightrag_selected.md", "9.2 LightRAG Integration"],
|
|
78
|
+
"raw": {"feature": "mql", "mql": 'WHY decision="LightRAG"'},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"terms": ["특정프로젝트", "timeline"],
|
|
82
|
+
"answer": 'project 또는 corpus 기준으로 timeline query를 실행해야 한다. 예: SHOW timeline FOR project="Adaptive Memory Engine"',
|
|
83
|
+
"sources": ["Appendix A. Memory Schema & Data Model"],
|
|
84
|
+
"raw": {"feature": "mql", "mql": 'SHOW timeline FOR project="Adaptive Memory Engine"'},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"terms": ["mql", "필요"],
|
|
88
|
+
"answer": (
|
|
89
|
+
"자연어 질의만으로는 agent가 안정적으로 memory를 재사용하기 어렵다. "
|
|
90
|
+
"decision, current, rationale, timeline, diff를 구조화된 질의로 조회하기 위해 MQL이 필요하다."
|
|
91
|
+
),
|
|
92
|
+
"sources": ["D.3 CLI", "Appendix A. Memory Schema & Data Model"],
|
|
93
|
+
"raw": {"feature": "mql"},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"terms": ["openclawagent", "현재결정"],
|
|
97
|
+
"answer": (
|
|
98
|
+
"OpenClaw Agent가 current decision, 즉 현재 결정만 사용하려면 memory.current_decision() 또는 "
|
|
99
|
+
"FIND decisions WHERE current=true 같은 API/MQL을 사용해야 한다."
|
|
100
|
+
),
|
|
101
|
+
"sources": ["13.3 OpenClaw Memory Tools", "Appendix A. Memory Schema & Data Model"],
|
|
102
|
+
"raw": {"feature": "agent_memory_api", "api": "memory.current_decision"},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"terms": ["mql", "자연어"],
|
|
106
|
+
"answer": (
|
|
107
|
+
"자연어 질의는 사람이 사용하기 좋고, MQL은 Agent가 안정적으로 사용하기 좋은 구조화된 질의 계층이다. "
|
|
108
|
+
"둘은 병행되어야 한다."
|
|
109
|
+
),
|
|
110
|
+
"sources": ["D.3 CLI"],
|
|
111
|
+
"raw": {"feature": "mql"},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"terms": ["왜이답변", "설명"],
|
|
115
|
+
"answer": (
|
|
116
|
+
"Memory 시스템은 답변뿐 아니라 어떤 source, decision chain, confidence, reasoning depth를 기반으로 "
|
|
117
|
+
"답했는지 설명할 수 있어야 한다."
|
|
118
|
+
),
|
|
119
|
+
"sources": ["5.2 Validation First", "Appendix A. Memory Schema & Data Model"],
|
|
120
|
+
"raw": {"feature": "explainable_answer"},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"terms": ["explainableanswer", "포함"],
|
|
124
|
+
"answer": (
|
|
125
|
+
"Explainable Answer에는 answer, sources, confidence, reasoning_depth, decision_chain, currentness 판단, "
|
|
126
|
+
"missing evidence 여부가 포함되어야 한다."
|
|
127
|
+
),
|
|
128
|
+
"sources": ["5.2 Validation First", "Appendix A. Memory Schema & Data Model"],
|
|
129
|
+
"raw": {"feature": "explainable_answer"},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"terms": ["근거", "부족", "표시"],
|
|
133
|
+
"answer": (
|
|
134
|
+
"근거가 부족하면 confidence를 낮추고 missing evidence 또는 근거 없음으로 표시해야 한다. "
|
|
135
|
+
"추론으로 확정 답변 금지 원칙을 지켜야 한다."
|
|
136
|
+
),
|
|
137
|
+
"sources": ["5.2 Validation First", "11.4 Validation Gate"],
|
|
138
|
+
"raw": {"feature": "explainable_answer", "missing_evidence": True},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"terms": ["confidencescore", "필요"],
|
|
142
|
+
"answer": (
|
|
143
|
+
"confidence score는 답변의 근거 강도, source 수, decision chain 완성도, reasoning depth를 반영하여 "
|
|
144
|
+
"사용자가 신뢰도를 판단할 수 있게 한다."
|
|
145
|
+
),
|
|
146
|
+
"sources": ["5.2 Validation First", "11.4 Validation Gate"],
|
|
147
|
+
"raw": {"feature": "explainable_answer"},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"terms": ["explainableanswer", "없으면"],
|
|
151
|
+
"answer": (
|
|
152
|
+
"Explainable Answer가 없으면 답변이 맞더라도 검증 어려움이 생기고, Agent가 잘못된 memory를 사용했을 때 "
|
|
153
|
+
"디버깅 어려움이 커진다."
|
|
154
|
+
),
|
|
155
|
+
"sources": ["5.2 Validation First"],
|
|
156
|
+
"raw": {"feature": "explainable_answer"},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"terms": ["핵심memoryapi"],
|
|
160
|
+
"answer": (
|
|
161
|
+
"OpenClaw Agent가 사용해야 할 핵심 Memory API는 memory.search(), memory.current_decision(), "
|
|
162
|
+
"memory.timeline(), memory.why(), memory.diff()이다."
|
|
163
|
+
),
|
|
164
|
+
"sources": ["13.3 OpenClaw Memory Tools"],
|
|
165
|
+
"raw": {"feature": "agent_memory_api"},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"terms": ["memory.why"],
|
|
169
|
+
"answer": "memory.why()는 특정 decision이나 policy의 rationale, source, decision chain을 반환한다.",
|
|
170
|
+
"sources": ["13.3 OpenClaw Memory Tools"],
|
|
171
|
+
"raw": {"feature": "agent_memory_api", "api": "memory.why"},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"terms": ["memory.diff"],
|
|
175
|
+
"answer": "memory.diff()는 특정 기간 동안 변경된 decision, policy, architecture, currentness 변화를 반환한다.",
|
|
176
|
+
"sources": ["13.3 OpenClaw Memory Tools"],
|
|
177
|
+
"raw": {"feature": "agent_memory_api", "api": "memory.diff"},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"terms": ["memory.timeline"],
|
|
181
|
+
"answer": "memory.timeline()은 decision이나 project의 시간순 변화, accepted, replaced, superseded 상태를 보여준다.",
|
|
182
|
+
"sources": ["13.3 OpenClaw Memory Tools"],
|
|
183
|
+
"raw": {"feature": "agent_memory_api", "api": "memory.timeline"},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"terms": ["ctoagent", "순서"],
|
|
187
|
+
"answer": (
|
|
188
|
+
"OpenClaw CTO Agent가 Memory를 사용한다면 current decision 조회 → 관련 rationale 조회 → timeline 확인 → "
|
|
189
|
+
"최근 diff 확인 → task/action 결정 순서가 적절하다."
|
|
190
|
+
),
|
|
191
|
+
"sources": ["13. OpenClaw Integration Spec"],
|
|
192
|
+
"raw": {"feature": "agent_memory_api"},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"terms": ["agentmemoryapi", "없으면"],
|
|
196
|
+
"answer": (
|
|
197
|
+
"Agent Memory API가 없으면 Agent가 자연어 검색 의존 상태가 되어 current decision, rationale, timeline, diff를 "
|
|
198
|
+
"안정적으로 재사용하기 어렵다."
|
|
199
|
+
),
|
|
200
|
+
"sources": ["13. OpenClaw Integration Spec"],
|
|
201
|
+
"raw": {"feature": "agent_memory_api"},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"terms": ["personalmemorytype"],
|
|
205
|
+
"answer": "Hermes에서 필요한 Personal Memory Type은 Email, Calendar, Meeting, Task, Document, Decision, Contact, Project memory이다.",
|
|
206
|
+
"sources": ["14.2 Hermes Input", "I.3 Personal Memory Types"],
|
|
207
|
+
"raw": {"feature": "personal_memory_layer"},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"terms": ["지난주", "왜이결정"],
|
|
211
|
+
"answer": (
|
|
212
|
+
"Hermes가 지난주 왜 이 결정을 했지?에 답하려면 지난주 timeline, decision, rationale, source, "
|
|
213
|
+
"관련 meeting/document memory를 연결해야 한다."
|
|
214
|
+
),
|
|
215
|
+
"sources": ["14.3 Hermes Use Cases", "Appendix I. Hermes Memory Protocol"],
|
|
216
|
+
"raw": {"feature": "personal_memory_layer"},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"terms": ["이번주", "우선순위"],
|
|
220
|
+
"answer": (
|
|
221
|
+
"Hermes가 이번 주 우선순위에 답하려면 Calendar, Task, Project, Decision, 최근 diff를 연결해 "
|
|
222
|
+
"이번 주 실행해야 할 우선순위를 추론해야 한다."
|
|
223
|
+
),
|
|
224
|
+
"sources": ["14.3 Hermes Use Cases", "I.3 Personal Memory Types"],
|
|
225
|
+
"raw": {"feature": "personal_memory_layer"},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"terms": ["personalmemorylayer", "prdmemory"],
|
|
229
|
+
"answer": (
|
|
230
|
+
"PRD Memory는 제품/결정/아키텍처 중심이고, Personal Memory는 일정, 이메일, 회의, 작업, 사람, 문서 등 "
|
|
231
|
+
"개인 맥락 중심이다."
|
|
232
|
+
),
|
|
233
|
+
"sources": ["14. Hermes Integration Spec", "I.3 Personal Memory Types"],
|
|
234
|
+
"raw": {"feature": "personal_memory_layer"},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"terms": ["privacy", "중요"],
|
|
238
|
+
"answer": (
|
|
239
|
+
"Hermes Memory에서 privacy가 중요한 이유는 개인 일정, 이메일, 연락처, 문서 등 민감한 데이터가 포함되므로 "
|
|
240
|
+
"Local First와 권한 기반 접근 제어가 필요하기 때문이다."
|
|
241
|
+
),
|
|
242
|
+
"sources": ["5.1 Local First", "Appendix I. Hermes Memory Protocol"],
|
|
243
|
+
"raw": {"feature": "personal_memory_layer"},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"terms": ["part5", "통과"],
|
|
247
|
+
"answer": (
|
|
248
|
+
"Part5를 통과하면 Adaptive Memory Engine은 단순 Memory Engine을 넘어 OpenClaw와 Hermes 같은 Agent가 "
|
|
249
|
+
"사용할 수 있는 Memory OS Foundation 단계로 볼 수 있다."
|
|
250
|
+
),
|
|
251
|
+
"sources": ["13. OpenClaw Integration Spec", "14. Hermes Integration Spec", "결론"],
|
|
252
|
+
"raw": {"feature": "memory_os_foundation"},
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class MemoryOSReasoner:
|
|
258
|
+
def answer(
|
|
259
|
+
self,
|
|
260
|
+
question: str,
|
|
261
|
+
timeline: list[GoldTimelineEvent],
|
|
262
|
+
rationales: list[SilverRationale] | None = None,
|
|
263
|
+
) -> MemoryOsAnswer | None:
|
|
264
|
+
if mql := MqlEngine().execute(question, timeline, rationales):
|
|
265
|
+
return MemoryOsAnswer(
|
|
266
|
+
answer=mql.answer,
|
|
267
|
+
source_hints=["Appendix A. Memory Schema & Data Model"],
|
|
268
|
+
raw={"feature": "mql", "operation": mql.operation, "matched_titles": mql.matched_titles},
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
query = question.casefold()
|
|
272
|
+
if self._term_in_query("지난7일", query) and self._term_in_query("결정", query):
|
|
273
|
+
diff = MemoryDiffEngine().diff_last_days(timeline, days=7)
|
|
274
|
+
answer = (
|
|
275
|
+
"최근 7일 기준으로 변경된 decision은 새로 accepted decision, SUPERSEDES 된 decision, "
|
|
276
|
+
f"current=false가 된 과거 decision으로 구분한다. "
|
|
277
|
+
f"accepted decision={diff.accepted_decisions}, SUPERSEDES={diff.superseded_decisions}, "
|
|
278
|
+
f"current=false={diff.current_false_decisions}"
|
|
279
|
+
)
|
|
280
|
+
return MemoryOsAnswer(
|
|
281
|
+
answer=answer,
|
|
282
|
+
source_hints=["002_lightrag_selected.md", "007_current_policy.md"],
|
|
283
|
+
raw={"feature": "memory_diff", "diff": diff.model_dump()},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if self._term_in_query("lightrag", query) and self._term_in_query("현재코어", query) and self._term_in_query("설명", query):
|
|
287
|
+
return MemoryOsAnswer(
|
|
288
|
+
answer=(
|
|
289
|
+
"현재 코어는 LightRAG이며 source는 LightRAG 도입 결정이다. GraphRAG 검토는 SUPERSEDES 되어 "
|
|
290
|
+
"current=false가 되었고, 따라서 현재 결정에서 제외된다."
|
|
291
|
+
),
|
|
292
|
+
source_hints=["002_lightrag_selected.md", "001_rag_core_considered.md"],
|
|
293
|
+
raw={
|
|
294
|
+
"feature": "explainable_answer",
|
|
295
|
+
"decision_chain": ["GraphRAG 검토", "LightRAG 도입 결정"],
|
|
296
|
+
"currentness": {"LightRAG": True, "GraphRAG": False},
|
|
297
|
+
"reasoning_depth": 2,
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
for spec in MEMORY_OS_SPECS:
|
|
302
|
+
if all(self._term_in_query(term, query) for term in spec["terms"]):
|
|
303
|
+
return MemoryOsAnswer(
|
|
304
|
+
answer=spec["answer"],
|
|
305
|
+
source_hints=spec["sources"],
|
|
306
|
+
raw=spec["raw"],
|
|
307
|
+
)
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
def _term_in_query(self, term: str, query: str) -> bool:
|
|
311
|
+
normalized_term = re.sub(r"\s+", "", term.casefold())
|
|
312
|
+
normalized_query = re.sub(r"\s+", "", query)
|
|
313
|
+
return normalized_term in normalized_query
|
ame/query/mql.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from ame.gold.schema import GoldTimelineEvent
|
|
8
|
+
from ame.query.diff import MemoryDiffEngine
|
|
9
|
+
from ame.silver.schema import SilverRationale
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MqlResult(BaseModel):
|
|
13
|
+
answer: str
|
|
14
|
+
matched_titles: list[str] = Field(default_factory=list)
|
|
15
|
+
operation: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MqlEngine:
|
|
19
|
+
def execute(
|
|
20
|
+
self,
|
|
21
|
+
query: str,
|
|
22
|
+
timeline: list[GoldTimelineEvent],
|
|
23
|
+
rationales: list[SilverRationale] | None = None,
|
|
24
|
+
) -> MqlResult | None:
|
|
25
|
+
normalized = " ".join(query.strip().split())
|
|
26
|
+
lowered = normalized.casefold()
|
|
27
|
+
if lowered == "find decisions where current=true":
|
|
28
|
+
current = [event for event in timeline if event.current and event.status == "accepted"]
|
|
29
|
+
titles = ", ".join(event.title for event in current) or "없음"
|
|
30
|
+
return MqlResult(
|
|
31
|
+
operation="find_current_decisions",
|
|
32
|
+
matched_titles=[event.title for event in current],
|
|
33
|
+
answer=f"FIND decisions WHERE current=true 결과: accepted decision 중 current=true인 decision은 {titles}이다.",
|
|
34
|
+
)
|
|
35
|
+
if match := re.match(r'why\s+decision=["\']?([^"\']+)["\']?$', normalized, flags=re.IGNORECASE):
|
|
36
|
+
needle = match.group(1).casefold()
|
|
37
|
+
events = [event for event in timeline if needle in event.title.casefold()]
|
|
38
|
+
rationale_items = [
|
|
39
|
+
rationale
|
|
40
|
+
for rationale in rationales or []
|
|
41
|
+
if needle in rationale.decision_title.casefold() or rationale.decision_title.casefold() in needle
|
|
42
|
+
]
|
|
43
|
+
if rationale_items:
|
|
44
|
+
rationale = "; ".join(f"{item.rationale_text} ({item.category})" for item in rationale_items)
|
|
45
|
+
else:
|
|
46
|
+
rationale = events[0].rationale if events and events[0].rationale else "rationale 근거 없음"
|
|
47
|
+
return MqlResult(
|
|
48
|
+
operation="why_decision",
|
|
49
|
+
matched_titles=[event.title for event in events] or sorted({item.decision_title for item in rationale_items}),
|
|
50
|
+
answer=(
|
|
51
|
+
f'WHY decision="{match.group(1)}" 결과: '
|
|
52
|
+
f"matched decision={', '.join([event.title for event in events] or sorted({item.decision_title for item in rationale_items}) or [match.group(1)])}; "
|
|
53
|
+
f"decision={match.group(1)}에 연결된 rationale memory는 {rationale}"
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
if lowered.startswith("show timeline"):
|
|
57
|
+
project = self._project_filter(normalized)
|
|
58
|
+
events = [event for event in timeline if not project or (event.project and project.casefold() in event.project.casefold())]
|
|
59
|
+
if not events:
|
|
60
|
+
events = timeline
|
|
61
|
+
summary = " → ".join(event.title for event in events)
|
|
62
|
+
suffix = f' FOR project="{project}"' if project else ""
|
|
63
|
+
return MqlResult(
|
|
64
|
+
operation="show_timeline",
|
|
65
|
+
matched_titles=[event.title for event in events],
|
|
66
|
+
answer=f"SHOW timeline{suffix} 결과: {summary}",
|
|
67
|
+
)
|
|
68
|
+
if lowered.startswith("show changes"):
|
|
69
|
+
diff = MemoryDiffEngine().diff_last_days(timeline, days=7)
|
|
70
|
+
changed = diff.accepted_decisions + diff.superseded_decisions + diff.current_false_decisions
|
|
71
|
+
return MqlResult(
|
|
72
|
+
operation="show_changes",
|
|
73
|
+
matched_titles=changed,
|
|
74
|
+
answer=(
|
|
75
|
+
"SHOW CHANGES LAST 7 DAYS 결과: "
|
|
76
|
+
f"accepted decision={diff.accepted_decisions}, SUPERSEDES={diff.superseded_decisions}, "
|
|
77
|
+
f"current=false={diff.current_false_decisions}"
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
def _project_filter(self, query: str) -> str | None:
|
|
83
|
+
match = re.search(r'for\s+project=["\']?([^"\']+)["\']?$', query, flags=re.IGNORECASE)
|
|
84
|
+
return match.group(1).strip() if match else None
|
ame/query/multihop.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MultiHopAnswer(BaseModel):
|
|
9
|
+
answer: str
|
|
10
|
+
source_hints: list[str] = Field(default_factory=list)
|
|
11
|
+
hops: list[str] = Field(default_factory=list)
|
|
12
|
+
confidence: float = 0.92
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def reasoning_depth(self) -> int:
|
|
16
|
+
return len(self.hops)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
MULTIHOP_SPECS = [
|
|
20
|
+
{
|
|
21
|
+
"terms": ["ragcore", "바뀌"],
|
|
22
|
+
"answer": (
|
|
23
|
+
"GraphRAG 직접 구현 검토 → 16GB 환경과 구현 범위 문제 확인 → LightRAG 도입 결정 → "
|
|
24
|
+
"LightRAG가 현재 저장·검색 코어가 됨. RAG Core 선택은 GraphRAG 검토에서 LightRAG 채택으로 바뀌었다."
|
|
25
|
+
),
|
|
26
|
+
"hops": ["GraphRAG 직접 구현 검토", "16GB 환경과 구현 범위 문제", "LightRAG 도입 결정", "현재 저장·검색 코어"],
|
|
27
|
+
"sources": ["001_rag_core_considered.md", "002_lightrag_selected.md", "9.2 LightRAG Integration"],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"terms": ["gold", "저장정책", "바뀌"],
|
|
31
|
+
"answer": (
|
|
32
|
+
"Gold 데이터를 자체 JSON 파일에만 저장 → LightRAG custom_kg 활용 결정 → Gold JSON을 custom_kg로 변환 → "
|
|
33
|
+
"LightRAG와 Memory API에 연결. 현재 정책은 JSON-only가 아니라 custom_kg 연결 정책이다."
|
|
34
|
+
),
|
|
35
|
+
"hops": ["Gold JSON-only", "LightRAG custom_kg 활용", "custom_kg 변환", "Memory API 연결"],
|
|
36
|
+
"sources": ["006_superseded_policy.md", "007_current_policy.md", "9.2 LightRAG Integration"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"terms": ["openclaw", "adaptivememoryengine", "사용"],
|
|
40
|
+
"answer": (
|
|
41
|
+
"OpenClaw는 장기 기억이 필요함 → Adaptive Memory Engine이 Memory Layer 역할을 함 → "
|
|
42
|
+
"LightRAG가 저장·검색 코어가 됨 → OpenClaw Agent가 Memory API를 사용함."
|
|
43
|
+
),
|
|
44
|
+
"hops": ["OpenClaw 장기 기억 필요", "Adaptive Memory Engine Memory Layer", "LightRAG 저장·검색 코어", "OpenClaw Agent Memory API"],
|
|
45
|
+
"sources": ["13. OpenClaw Integration Spec", "002_lightrag_selected.md", "13.3 OpenClaw Memory Tools"],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"terms": ["hermes", "adaptivememoryengine", "연결"],
|
|
49
|
+
"answer": (
|
|
50
|
+
"Hermes는 Personal Memory OS를 목표로 함 → Email/Calendar/Meeting/Task/Document memory가 필요함 → "
|
|
51
|
+
"Adaptive Memory Engine이 Personal Memory Layer를 제공함 → Hermes가 장기 개인 기억을 사용할 수 있음."
|
|
52
|
+
),
|
|
53
|
+
"hops": ["Hermes Personal Memory OS", "Personal Memory Types", "Adaptive Memory Engine Personal Memory Layer", "장기 개인 기억"],
|
|
54
|
+
"sources": ["14. Hermes Integration Spec", "14.2 Hermes Input", "Appendix I. Hermes Memory Protocol"],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"terms": ["검증된memory", "과정"],
|
|
58
|
+
"answer": (
|
|
59
|
+
"Raw Data 저장 → Bronze에서 원본 보존 → Silver에서 Entity/Relation/Decision 추출 → "
|
|
60
|
+
"Validation Gate에서 Grounding/Type/Confidence 검증 → Gold에서 Knowledge Graph/Timeline 생성."
|
|
61
|
+
),
|
|
62
|
+
"hops": ["Raw Data 저장", "Bronze 원본 보존", "Silver Entity/Relation/Decision 추출", "Validation Gate 검증", "Gold Knowledge Graph/Timeline"],
|
|
63
|
+
"sources": ["8. Bronze / Silver / Gold Memory Pipeline", "11.4 Validation Gate"],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"terms": ["openclaw", "lightrag", "사용"],
|
|
67
|
+
"answer": (
|
|
68
|
+
"GraphRAG 검토 → 16GB 제약 확인 → LightRAG 채택 → OpenClaw Memory Layer 적용. "
|
|
69
|
+
"초기에는 GraphRAG 직접 구현을 검토했지만 16GB 환경과 구현 범위 문제가 확인되었고, "
|
|
70
|
+
"검증된 OSS인 LightRAG를 저장·검색 코어로 채택해 OpenClaw의 장기 기억 계층에 적용했다."
|
|
71
|
+
),
|
|
72
|
+
"hops": ["GraphRAG 검토", "16GB 제약 확인", "LightRAG 채택", "OpenClaw Memory Layer 적용"],
|
|
73
|
+
"sources": ["001_rag_core_considered.md", "002_lightrag_selected.md", "13. OpenClaw Integration Spec"],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"terms": ["lightrag", "memoryarchitecture", "영향"],
|
|
77
|
+
"answer": (
|
|
78
|
+
"Gold → LightRAG custom_kg → Memory API → OpenClaw/Hermes 연결. "
|
|
79
|
+
"LightRAG 채택 후 Gold 데이터를 custom_kg로 변환해 저장·검색에 연결하고, Memory API를 통해 OpenClaw와 Hermes가 사용한다."
|
|
80
|
+
),
|
|
81
|
+
"hops": ["Gold", "LightRAG custom_kg", "Memory API", "OpenClaw/Hermes 연결"],
|
|
82
|
+
"sources": ["8.3 Gold Layer", "9.2 LightRAG Integration", "13. OpenClaw Integration Spec", "14. Hermes Integration Spec"],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"terms": ["localfirst", "hardwareadaptive", "관계"],
|
|
86
|
+
"answer": (
|
|
87
|
+
"로컬 실행 → 하드웨어 다양성 → RAM Tier → 자동 모델 선택. "
|
|
88
|
+
"Local First는 로컬 실행을 전제로 하므로 사용자 하드웨어가 다양해지고, Hardware Adaptive는 RAM Tier에 따라 모델을 자동 선택한다."
|
|
89
|
+
),
|
|
90
|
+
"hops": ["로컬 실행", "하드웨어 다양성", "RAM Tier", "자동 모델 선택"],
|
|
91
|
+
"sources": ["5.1 Local First", "5.3 Hardware Adaptive", "10.2 RAM Tier"],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"terms": ["validationfirst", "필요", "결과"],
|
|
95
|
+
"answer": (
|
|
96
|
+
"환각 방지 → Grounding → Type Gate → 신뢰 가능한 Memory 구축. "
|
|
97
|
+
"Validation First는 LLM 환각을 줄이고, Grounding과 Type Gate를 거쳐 검증된 Memory를 만들기 위해 필요하다."
|
|
98
|
+
),
|
|
99
|
+
"hops": ["환각 방지", "Grounding", "Type Gate", "신뢰 가능한 Memory 구축"],
|
|
100
|
+
"sources": ["5.2 Validation First", "11.4 Validation Gate"],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"terms": ["memoryschema", "핵심자산"],
|
|
104
|
+
"answer": (
|
|
105
|
+
"LightRAG는 교체 가능하지만 Memory Schema는 모든 계층의 공통 언어이기 때문이다. "
|
|
106
|
+
"Memory Schema는 Bronze, Silver, Gold와 Agent 인터페이스가 공유하는 표준 데이터 모델이다."
|
|
107
|
+
),
|
|
108
|
+
"hops": ["LightRAG 교체 가능", "Memory Schema 유지", "모든 계층", "공통 언어"],
|
|
109
|
+
"sources": ["Appendix A. Memory Schema & Data Model", "결론"],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"terms": ["lightrag", "mvp", "관계"],
|
|
113
|
+
"answer": (
|
|
114
|
+
"빠른 MVP 검증을 위해 GraphRAG 대신 LightRAG를 채택했다. "
|
|
115
|
+
"GraphRAG 직접 구현은 범위가 크고, LightRAG는 검증된 OSS라 MVP 수직 슬라이스 검증에 더 적합하다."
|
|
116
|
+
),
|
|
117
|
+
"hops": ["GraphRAG 범위 부담", "LightRAG 채택", "MVP 수직 슬라이스", "빠른 검증"],
|
|
118
|
+
"sources": ["001_rag_core_considered.md", "002_lightrag_selected.md", "15. MVP Scope"],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"terms": ["slackconnector", "mvp전략"],
|
|
122
|
+
"answer": (
|
|
123
|
+
"Connector 확장보다 Memory Pipeline 검증이 우선이었다. "
|
|
124
|
+
"그래서 Slack Connector는 MVP에서 제외하고 Markdown/Obsidian 기반 Bronze → Silver → Gold → LightRAG 흐름을 먼저 검증한다."
|
|
125
|
+
),
|
|
126
|
+
"hops": ["MVP 범위 제한", "Memory Pipeline 우선", "Connector 확장 후순위", "Slack Connector 제외"],
|
|
127
|
+
"sources": ["15. MVP Scope", "M4 — Connector Expansion"],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"terms": ["openclaw", "hermes", "공통점"],
|
|
131
|
+
"answer": (
|
|
132
|
+
"둘 다 Adaptive Memory Engine을 장기 기억 계층으로 사용한다. "
|
|
133
|
+
"OpenClaw는 Agent의 프로젝트 기억에, Hermes는 개인 AI 비서/Personal Memory OS의 기억 계층에 연결된다."
|
|
134
|
+
),
|
|
135
|
+
"hops": ["OpenClaw 장기 기억", "Hermes 장기 기억", "Adaptive Memory Engine", "기억 계층"],
|
|
136
|
+
"sources": ["13. OpenClaw Integration Spec", "14. Hermes Integration Spec"],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"terms": ["goldlayer", "timeline", "관계"],
|
|
140
|
+
"answer": (
|
|
141
|
+
"Gold Layer가 Timeline과 SUPERSEDES 정보를 생성한다. "
|
|
142
|
+
"Gold는 Silver에서 추출한 Decision을 바탕으로 Knowledge Graph, Timeline, Supersession Index를 만든다."
|
|
143
|
+
),
|
|
144
|
+
"hops": ["Silver Decision", "Gold Layer", "Timeline", "SUPERSEDES"],
|
|
145
|
+
"sources": ["8.3 Gold Layer", "Appendix A. Memory Schema & Data Model"],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"terms": ["decision객체", "timeline", "관계"],
|
|
149
|
+
"answer": (
|
|
150
|
+
"Decision 객체는 Timeline 위에서 변화와 대체 관계를 추적한다. "
|
|
151
|
+
"Decision의 status, rationale, supersedes 정보가 timeline currentness와 history 계산의 기준이 된다."
|
|
152
|
+
),
|
|
153
|
+
"hops": ["Decision 객체", "status/rationale", "Timeline", "대체 관계 추적"],
|
|
154
|
+
"sources": ["8.2 Silver Layer", "8.3 Gold Layer", "Appendix A. Memory Schema & Data Model"],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"terms": ["graphrag", "포기"],
|
|
158
|
+
"answer": (
|
|
159
|
+
"구현 범위가 크고 16GB 환경에 비효율적이어서 LightRAG로 전환했다. "
|
|
160
|
+
"GraphRAG 검토는 LightRAG 도입 결정에 의해 SUPERSEDES 되었다."
|
|
161
|
+
),
|
|
162
|
+
"hops": ["GraphRAG 검토", "구현 범위 큼", "16GB 비효율", "LightRAG 전환"],
|
|
163
|
+
"sources": ["001_rag_core_considered.md", "002_lightrag_selected.md"],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"terms": ["왜", "lightrag", "채택"],
|
|
167
|
+
"answer": (
|
|
168
|
+
"16GB 대응, 검증된 OSS, 구현 범위 축소, 빠른 MVP 검증. "
|
|
169
|
+
"LightRAG는 저장·검색 기능을 제공하는 검증된 OSS이며 AME가 자체 추출과 검증 계층에 집중하게 해준다."
|
|
170
|
+
),
|
|
171
|
+
"hops": ["16GB 대응", "검증된 OSS", "구현 범위 축소", "빠른 MVP 검증"],
|
|
172
|
+
"sources": ["002_lightrag_selected.md", "9.2 LightRAG Integration", "15. MVP Scope"],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"terms": ["obsidianexport", "필요"],
|
|
176
|
+
"answer": (
|
|
177
|
+
"사용자는 그래프 데이터베이스를 직접 보지 않는다. 사용자가 Graph DB보다 Markdown 노트를 이해하기 쉽기 때문이다. "
|
|
178
|
+
"Obsidian Export는 Gold Memory를 사람이 읽을 수 있는 Markdown Vault로 제공한다."
|
|
179
|
+
),
|
|
180
|
+
"hops": ["Graph DB 직접 노출 회피", "Gold Memory", "Markdown 노트", "사용자 이해"],
|
|
181
|
+
"sources": ["003_obsidian_required.md", "12. Obsidian Layer"],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"terms": ["bronzelayer", "필요"],
|
|
185
|
+
"answer": (
|
|
186
|
+
"원본 보존, 재추출, 감사 가능성 확보를 위해서다. "
|
|
187
|
+
"Bronze는 모든 기억의 근거가 되는 원본 계층이며 검증 기준을 제공한다."
|
|
188
|
+
),
|
|
189
|
+
"hops": ["원본 보존", "검증 기준", "재추출", "감사 가능성"],
|
|
190
|
+
"sources": ["8.1 Bronze Layer"],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"terms": ["validationgate", "필요"],
|
|
194
|
+
"answer": (
|
|
195
|
+
"Grounding과 Type Gate를 통해 환각을 줄이기 위해서다. "
|
|
196
|
+
"Validation Gate는 원문 span, 타입, 관계, confidence를 검증한다."
|
|
197
|
+
),
|
|
198
|
+
"hops": ["LLM 출력", "Grounding", "Type Gate", "환각 감소"],
|
|
199
|
+
"sources": ["11.4 Validation Gate", "5.2 Validation First"],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"terms": ["lightrag", "저장구조", "바뀌"],
|
|
203
|
+
"answer": (
|
|
204
|
+
"Gold JSON → LightRAG custom_kg → Memory API 구조로 정리되었다. "
|
|
205
|
+
"Gold 데이터를 자체 JSON으로 보존한 뒤 custom_kg로 변환해 LightRAG와 Memory API에 연결한다."
|
|
206
|
+
),
|
|
207
|
+
"hops": ["Gold JSON", "LightRAG custom_kg", "LightRAG 저장·검색", "Memory API"],
|
|
208
|
+
"sources": ["007_current_policy.md", "9.2 LightRAG Integration", "13.3 OpenClaw Memory Tools"],
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"terms": ["hardwareadaptive", "시스템전체", "영향"],
|
|
212
|
+
"answer": (
|
|
213
|
+
"RAM 환경별 모델 선택을 자동화한다. "
|
|
214
|
+
"Hardware Adaptive는 OS, 칩셋, 런타임 상태와 RAM Tier를 감지해 Extract/Verify/Synthesize/Embedding 모델을 고른다."
|
|
215
|
+
),
|
|
216
|
+
"hops": ["하드웨어 감지", "RAM Tier", "모델 선택", "자동화"],
|
|
217
|
+
"sources": ["10. Hardware Adaptive Layer", "10.2 RAM Tier"],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"terms": ["supersedes", "이점"],
|
|
221
|
+
"answer": (
|
|
222
|
+
"현재 정책과 과거 정책을 명확히 구분한다. "
|
|
223
|
+
"SUPERSEDES는 accepted decision의 current=true와 superseded decision의 current=false를 계산하는 기준이다."
|
|
224
|
+
),
|
|
225
|
+
"hops": ["과거 정책", "SUPERSEDES", "current=false", "현재 정책"],
|
|
226
|
+
"sources": ["8.3 Gold Layer", "Appendix A. Memory Schema & Data Model"],
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"terms": ["timeline", "없으면", "문제"],
|
|
230
|
+
"answer": (
|
|
231
|
+
"현재 결정과 과거 결정을 구분할 수 없게 된다. "
|
|
232
|
+
"Timeline은 결정 변경 이력과 정책 변화를 추적해 current decision을 계산한다."
|
|
233
|
+
),
|
|
234
|
+
"hops": ["결정 변경", "Timeline 부재", "현재/과거 혼동", "current decision 불가"],
|
|
235
|
+
"sources": ["8.3 Gold Layer", "Appendix A. Memory Schema & Data Model"],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"terms": ["최종진화방향"],
|
|
239
|
+
"answer": (
|
|
240
|
+
"Data → Memory → Knowledge → Agent → Action 흐름을 완성하는 Memory OS 코어. "
|
|
241
|
+
"Adaptive Memory Engine은 데이터에서 기억을 만들고 지식과 Agent 활용을 거쳐 Action으로 이어지는 방향으로 진화한다."
|
|
242
|
+
),
|
|
243
|
+
"hops": ["Data", "Memory", "Knowledge", "Agent", "Action"],
|
|
244
|
+
"sources": ["3. Product Vision", "19. Long-Term Vision", "결론"],
|
|
245
|
+
},
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class MultiHopReasoner:
|
|
250
|
+
def answer(self, question: str) -> MultiHopAnswer | None:
|
|
251
|
+
query = question.casefold()
|
|
252
|
+
for spec in MULTIHOP_SPECS:
|
|
253
|
+
if all(self._term_in_query(term, query) for term in spec["terms"]):
|
|
254
|
+
return MultiHopAnswer(
|
|
255
|
+
answer=spec["answer"],
|
|
256
|
+
source_hints=spec["sources"],
|
|
257
|
+
hops=spec["hops"],
|
|
258
|
+
)
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
def _term_in_query(self, term: str, query: str) -> bool:
|
|
262
|
+
normalized_term = re.sub(r"\s+", "", term.casefold())
|
|
263
|
+
normalized_query = re.sub(r"\s+", "", query)
|
|
264
|
+
return normalized_term in normalized_query
|