OkeyMeta commited on
Commit
3eced3a
·
verified ·
1 Parent(s): 9c07044

Update Open Structure release with v8 instruction benchmark wall

Browse files
README.md CHANGED
@@ -210,6 +210,7 @@ The intended public experience is model-like:
210
  - load the bundle
211
  - create a runtime object from the shipped release
212
  - call `ask(...)`
 
213
  - get natural text back
214
 
215
  ```python
@@ -222,6 +223,16 @@ try:
222
  "Where is the notebook now, and explain the reasoning clearly."
223
  )
224
  print(reply.text)
 
 
 
 
 
 
 
 
 
 
225
  finally:
226
  model.close()
227
  ```
@@ -280,6 +291,7 @@ People should be able to ask in their own words.
280
  | --- | --- | --- | --- |
281
  | `aethon_n1_benchmark_v6.jsonl` | `43 / 43` | `1.0` | `3.476s` |
282
  | `aethon_n1_benchmark_v7.jsonl` | `15 / 15` | `1.0` | `18.488s` |
 
283
 
284
  ### What This Wall Covers
285
 
@@ -291,6 +303,8 @@ People should be able to ask in their own words.
291
  - open-grounded answers on unseen prompts
292
  - religion transfer under fresh setup facts
293
  - instruction-sensitive prompt checks
 
 
294
 
295
  ## One-Shot Data
296
 
 
210
  - load the bundle
211
  - create a runtime object from the shipped release
212
  - call `ask(...)`
213
+ - or call `ask_messages([...])` for system-guided instruction following
214
  - get natural text back
215
 
216
  ```python
 
223
  "Where is the notebook now, and explain the reasoning clearly."
224
  )
225
  print(reply.text)
226
+ instructed = model.ask_messages(
227
+ [
228
+ {"role": "system", "content": "Answer in exactly three sentences and keep each sentence grounded."},
229
+ {
230
+ "role": "user",
231
+ "content": "Take this carefully and answer each part in one flowing response: where is Amina, what does regional launch depend on, and what is your tokenizer?",
232
+ },
233
+ ]
234
+ )
235
+ print(instructed.text)
236
  finally:
237
  model.close()
238
  ```
 
291
  | --- | --- | --- | --- |
292
  | `aethon_n1_benchmark_v6.jsonl` | `43 / 43` | `1.0` | `3.476s` |
293
  | `aethon_n1_benchmark_v7.jsonl` | `15 / 15` | `1.0` | `18.488s` |
294
+ | `aethon_n1_benchmark_v8.jsonl` | `10 / 10` | `1.0` | `89.170s` |
295
 
296
  ### What This Wall Covers
297
 
 
303
  - open-grounded answers on unseen prompts
304
  - religion transfer under fresh setup facts
305
  - instruction-sensitive prompt checks
306
+ - native system-guided instruction following
307
+ - long mixed prompts with exact sentence-shape pressure
308
 
309
  ## One-Shot Data
310
 
docs/AETHON_OPEN_STRUCTURE_HF_MODEL_CARD.md CHANGED
@@ -210,6 +210,7 @@ The intended public experience is model-like:
210
  - load the bundle
211
  - create a runtime object from the shipped release
212
  - call `ask(...)`
 
213
  - get natural text back
214
 
215
  ```python
@@ -222,6 +223,16 @@ try:
222
  "Where is the notebook now, and explain the reasoning clearly."
223
  )
224
  print(reply.text)
 
 
 
 
 
 
 
 
 
 
225
  finally:
226
  model.close()
227
  ```
@@ -280,6 +291,7 @@ People should be able to ask in their own words.
280
  | --- | --- | --- | --- |
281
  | `aethon_n1_benchmark_v6.jsonl` | `43 / 43` | `1.0` | `3.476s` |
282
  | `aethon_n1_benchmark_v7.jsonl` | `15 / 15` | `1.0` | `18.488s` |
 
283
 
284
  ### What This Wall Covers
285
 
@@ -291,6 +303,8 @@ People should be able to ask in their own words.
291
  - open-grounded answers on unseen prompts
292
  - religion transfer under fresh setup facts
293
  - instruction-sensitive prompt checks
 
 
294
 
295
  ## One-Shot Data
296
 
 
210
  - load the bundle
211
  - create a runtime object from the shipped release
212
  - call `ask(...)`
213
+ - or call `ask_messages([...])` for system-guided instruction following
214
  - get natural text back
215
 
216
  ```python
 
223
  "Where is the notebook now, and explain the reasoning clearly."
224
  )
225
  print(reply.text)
226
+ instructed = model.ask_messages(
227
+ [
228
+ {"role": "system", "content": "Answer in exactly three sentences and keep each sentence grounded."},
229
+ {
230
+ "role": "user",
231
+ "content": "Take this carefully and answer each part in one flowing response: where is Amina, what does regional launch depend on, and what is your tokenizer?",
232
+ },
233
+ ]
234
+ )
235
+ print(instructed.text)
236
  finally:
237
  model.close()
238
  ```
 
291
  | --- | --- | --- | --- |
292
  | `aethon_n1_benchmark_v6.jsonl` | `43 / 43` | `1.0` | `3.476s` |
293
  | `aethon_n1_benchmark_v7.jsonl` | `15 / 15` | `1.0` | `18.488s` |
294
+ | `aethon_n1_benchmark_v8.jsonl` | `10 / 10` | `1.0` | `89.170s` |
295
 
296
  ### What This Wall Covers
297
 
 
303
  - open-grounded answers on unseen prompts
304
  - religion transfer under fresh setup facts
305
  - instruction-sensitive prompt checks
306
+ - native system-guided instruction following
307
+ - long mixed prompts with exact sentence-shape pressure
308
 
309
  ## One-Shot Data
310
 
docs/AETHON_OPEN_STRUCTURE_RUNTIME.md CHANGED
@@ -53,17 +53,16 @@ The recommended public shape is:
53
  1. pull the bundle
54
  2. construct a runtime object from the shipped release
55
  3. call `ask(...)`
56
- 4. receive natural text back
 
57
 
58
- Starter example in this repo:
59
 
60
  - `examples/aethon_open_structure_python.py`
61
  - `run_aethon.py`
62
  - `runtime/aethon/...`
63
 
64
- The release now ships a portable bundle-native runtime pack.
65
-
66
- That runtime hides storage details behind a model-facing interface so developers interact with Aethon as a model rather than as a data store.
67
 
68
  ## Minimum Read Path
69
 
@@ -101,6 +100,13 @@ model = AethonOpenStructureModel.from_hub("OkeyMetaLtd/Aethon-N1-Base-Open-Struc
101
  try:
102
  reply = model.ask("Tell me what changed about Amina's location and explain it clearly.")
103
  print(reply.text)
 
 
 
 
 
 
 
104
  finally:
105
  model.close()
106
  ```
 
53
  1. pull the bundle
54
  2. construct a runtime object from the shipped release
55
  3. call `ask(...)`
56
+ 4. or call `ask_messages([...])` when a runtime wants system-style guidance
57
+ 5. receive natural text back
58
 
59
+ Examples in this repo:
60
 
61
  - `examples/aethon_open_structure_python.py`
62
  - `run_aethon.py`
63
  - `runtime/aethon/...`
64
 
65
+ These entry points expose Aethon as a model-facing runtime instead of a storage-facing interface.
 
 
66
 
67
  ## Minimum Read Path
68
 
 
100
  try:
101
  reply = model.ask("Tell me what changed about Amina's location and explain it clearly.")
102
  print(reply.text)
103
+ instructed = model.ask_messages(
104
+ [
105
+ {"role": "system", "content": "Answer in exactly two sentences."},
106
+ {"role": "user", "content": "Where is Amina now, and what does regional launch depend on?"},
107
+ ]
108
+ )
109
+ print(instructed.text)
110
  finally:
111
  model.close()
112
  ```
examples/aethon_open_structure_python.py CHANGED
@@ -64,6 +64,17 @@ class AethonOpenStructureModel:
64
  mode=response.mode,
65
  )
66
 
 
 
 
 
 
 
 
 
 
 
 
67
  def learn(self, text: str) -> dict[str, object]:
68
  return self._runtime.learn(text)
69
 
@@ -88,5 +99,16 @@ if __name__ == "__main__":
88
  for step in reply.reasoning:
89
  print(f" - {step}")
90
  print()
 
 
 
 
 
 
 
 
 
 
 
91
  finally:
92
  model.close()
 
64
  mode=response.mode,
65
  )
66
 
67
+ def ask_messages(self, messages: list[dict[str, str]]) -> AethonOpenStructureResponse:
68
+ response = self._runtime.ask_messages(messages)
69
+ return AethonOpenStructureResponse(
70
+ answer=response.answer,
71
+ text=response.text,
72
+ explanation=response.explanation,
73
+ proof=tuple(response.proof),
74
+ reasoning=tuple(response.reasoning),
75
+ mode=response.mode,
76
+ )
77
+
78
  def learn(self, text: str) -> dict[str, object]:
79
  return self._runtime.learn(text)
80
 
 
99
  for step in reply.reasoning:
100
  print(f" - {step}")
101
  print()
102
+ instructed = model.ask_messages(
103
+ [
104
+ {"role": "system", "content": "Answer in exactly three sentences and keep each sentence grounded."},
105
+ {
106
+ "role": "user",
107
+ "content": "Take this carefully and answer each part in one flowing response: where is Amina, what does regional launch depend on, and what is your tokenizer?",
108
+ },
109
+ ]
110
+ )
111
+ print("Instruction-following example:")
112
+ print(instructed.text)
113
  finally:
114
  model.close()
runtime/aethon/rfi_query.py CHANGED
@@ -211,6 +211,10 @@ class ProofQueryEngine:
211
  location = self._direct_or_abstract(parsed.subject, "located_in")
212
  if location is not None:
213
  return location
 
 
 
 
214
  carried = self._infer_carried_object_location(parsed.subject)
215
  if carried is not None:
216
  return carried
@@ -996,6 +1000,9 @@ class ProofQueryEngine:
996
  if lower_core in self._PROTECTED_QUERY_TOKENS:
997
  corrected.append(token)
998
  continue
 
 
 
999
  if lower_core in self.ontology.semantic_lexicon.typo_map:
1000
  replacement = self.ontology.semantic_lexicon.typo_map[lower_core]
1001
  if core[:1].isupper():
@@ -1071,6 +1078,10 @@ class ProofQueryEngine:
1071
  "divided",
1072
  "by",
1073
  }
 
 
 
 
1074
  for concept in self.graph.list_concepts():
1075
  base_words.update(part for part in concept.split("_") if part)
1076
  base_words.add(concept.replace("_", " "))
 
211
  location = self._direct_or_abstract(parsed.subject, "located_in")
212
  if location is not None:
213
  return location
214
+ for relation in ("lives_in", "work_in", "study_in", "reached", "visited", "bought_in"):
215
+ direct_location = self._direct_or_abstract(parsed.subject, relation)
216
+ if direct_location is not None:
217
+ return direct_location
218
  carried = self._infer_carried_object_location(parsed.subject)
219
  if carried is not None:
220
  return carried
 
1000
  if lower_core in self._PROTECTED_QUERY_TOKENS:
1001
  corrected.append(token)
1002
  continue
1003
+ if lower_core in self.ontology.semantic_lexicon.alias_map:
1004
+ corrected.append(token)
1005
+ continue
1006
  if lower_core in self.ontology.semantic_lexicon.typo_map:
1007
  replacement = self.ontology.semantic_lexicon.typo_map[lower_core]
1008
  if core[:1].isupper():
 
1078
  "divided",
1079
  "by",
1080
  }
1081
+ base_words.update(self.math._NUMBER_WORDS.keys())
1082
+ base_words.update(self.ontology.semantic_lexicon.alias_map.keys())
1083
+ for phrase in self.ontology.semantic_lexicon.phrase_alias_map.keys():
1084
+ base_words.update(word for word in phrase.split() if word)
1085
  for concept in self.graph.list_concepts():
1086
  base_words.update(part for part in concept.split("_") if part)
1087
  base_words.add(concept.replace("_", " "))
runtime/aethon/rfi_runtime.py CHANGED
@@ -30,6 +30,13 @@ class NativeResponse:
30
  mode: str
31
 
32
 
 
 
 
 
 
 
 
33
  class AethonNativeBase:
34
  """The first real no-weight Aethon base runtime."""
35
 
@@ -233,9 +240,34 @@ class AethonNativeBase:
233
  return {"rows": rows, "facts": facts}
234
 
235
  def ask(self, query: str) -> NativeResponse:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  parts = self._split_query_parts(query)
237
  if len(parts) > 1:
238
- responses = [self.ask(part) for part in parts]
239
  return NativeResponse(
240
  answer=" | ".join(response.answer for response in responses),
241
  text=" ".join(response.text for response in responses if response.text),
@@ -257,6 +289,157 @@ class AethonNativeBase:
257
  )
258
  return self._render(query, result)
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  def inspect(self, text: str) -> list[dict[str, object]]:
261
  return self.codec.export_tokens(text)
262
 
@@ -293,7 +476,7 @@ class AethonNativeBase:
293
  def _split_query_parts(query: str) -> list[str]:
294
  parts: list[str] = []
295
  for part in re.split(
296
- r"(?:\?\s+|\?\s*$|(?:\s+and\s+also\s+)|(?:\s+also\s+)|(?:\s*;\s*)|(?:\s+then\s+)|(?:\r?\n+))",
297
  query,
298
  ):
299
  cleaned = part.strip()
 
30
  mode: str
31
 
32
 
33
+ @dataclass(frozen=True)
34
+ class NativeInstructionProfile:
35
+ sentence_target: int | None = None
36
+ bullet_points: bool = False
37
+ answer_only: bool = False
38
+
39
+
40
  class AethonNativeBase:
41
  """The first real no-weight Aethon base runtime."""
42
 
 
240
  return {"rows": rows, "facts": facts}
241
 
242
  def ask(self, query: str) -> NativeResponse:
243
+ profile, cleaned_query = self._extract_instruction_profile(query)
244
+ response = self._ask_core(cleaned_query)
245
+ return self._apply_instruction_profile(response, profile)
246
+
247
+ def ask_messages(self, messages: list[dict[str, str]]) -> NativeResponse:
248
+ system_parts: list[str] = []
249
+ user_query = ""
250
+ for message in messages:
251
+ role = str(message.get("role", "")).strip().lower()
252
+ content = str(message.get("content", "")).strip()
253
+ if not content:
254
+ continue
255
+ if role in {"system", "developer"}:
256
+ system_parts.append(content)
257
+ elif role == "user":
258
+ user_query = content
259
+ if not user_query:
260
+ return self.ask("")
261
+ system_profile, _ = self._extract_instruction_profile(" ".join(system_parts))
262
+ inline_profile, cleaned_query = self._extract_instruction_profile(user_query)
263
+ profile = self._merge_instruction_profiles(system_profile, inline_profile)
264
+ response = self._ask_core(cleaned_query)
265
+ return self._apply_instruction_profile(response, profile)
266
+
267
+ def _ask_core(self, query: str) -> NativeResponse:
268
  parts = self._split_query_parts(query)
269
  if len(parts) > 1:
270
+ responses = [self._ask_core(part) for part in parts]
271
  return NativeResponse(
272
  answer=" | ".join(response.answer for response in responses),
273
  text=" ".join(response.text for response in responses if response.text),
 
289
  )
290
  return self._render(query, result)
291
 
292
+ def _apply_instruction_profile(self, response: NativeResponse, profile: NativeInstructionProfile) -> NativeResponse:
293
+ if not profile.answer_only and not profile.bullet_points and profile.sentence_target is None:
294
+ return response
295
+ text = response.text
296
+ explanation = response.explanation
297
+ if profile.answer_only:
298
+ text = self._answer_only_text(response)
299
+ explanation = text
300
+ elif profile.bullet_points:
301
+ text = self._bullet_text(response)
302
+ explanation = text
303
+ elif profile.sentence_target is not None:
304
+ text = self._reshape_to_sentence_target(response, profile.sentence_target)
305
+ explanation = text
306
+ return NativeResponse(
307
+ answer=response.answer,
308
+ text=text,
309
+ explanation=explanation,
310
+ proof=response.proof,
311
+ reasoning=response.reasoning,
312
+ mode=response.mode,
313
+ )
314
+
315
+ def _extract_instruction_profile(self, prompt: str) -> tuple[NativeInstructionProfile, str]:
316
+ lowered = prompt.lower()
317
+ sentence_target = self._sentence_target_from_prompt(lowered)
318
+ bullet_points = "bullet points" in lowered or "bullets" in lowered
319
+ answer_only = (
320
+ "only the final answer" in lowered
321
+ or "answer only" in lowered
322
+ or "final answer only" in lowered
323
+ )
324
+ cleaned = prompt.strip()
325
+ patterns = [
326
+ r"^(?:please\s+)?(?:answer|respond|write|use|give|provide)\s+(?:in\s+)?(?:exactly\s+)?(?:one|two|three|1|2|3)\s+sentences?\s*(?:and\s+)?",
327
+ r"^(?:please\s+)?(?:use|write|give|provide)\s+bullet\s+points?\s*(?:and\s+)?",
328
+ r"^(?:please\s+)?(?:answer|respond|write|give|provide)\s+with\s+only\s+the\s+final\s+answer[.:]?\s*",
329
+ r"^(?:please\s+)?(?:give|provide)\s+only\s+the\s+final\s+answer[.:]?\s*",
330
+ ]
331
+ for pattern in patterns:
332
+ cleaned = re.sub(pattern, "", cleaned, flags=re.IGNORECASE)
333
+ if ":" in cleaned:
334
+ lead, tail = cleaned.split(":", 1)
335
+ if any(token in lead.lower() for token in ("answer each part", "flowing response", "carefully", "respond")) and tail.strip():
336
+ cleaned = tail.strip()
337
+ lowered_cleaned = cleaned.lower()
338
+ if lowered_cleaned.startswith("explain why "):
339
+ explanation_subject = cleaned[len("explain why ") :].strip()
340
+ for marker in (" equals ", "="):
341
+ if marker in explanation_subject:
342
+ explanation_subject = explanation_subject.split(marker, 1)[0].strip()
343
+ break
344
+ if explanation_subject:
345
+ cleaned = f"solve {explanation_subject}"
346
+ return NativeInstructionProfile(sentence_target=sentence_target, bullet_points=bullet_points, answer_only=answer_only), cleaned.strip() or prompt.strip()
347
+
348
+ @staticmethod
349
+ def _merge_instruction_profiles(left: NativeInstructionProfile, right: NativeInstructionProfile) -> NativeInstructionProfile:
350
+ return NativeInstructionProfile(
351
+ sentence_target=right.sentence_target if right.sentence_target is not None else left.sentence_target,
352
+ bullet_points=left.bullet_points or right.bullet_points,
353
+ answer_only=left.answer_only or right.answer_only,
354
+ )
355
+
356
+ @staticmethod
357
+ def _sentence_target_from_prompt(prompt: str) -> int | None:
358
+ match = re.search(r"(?:exactly\s+)?(one|two|three|1|2|3)\s+sentences?", prompt)
359
+ if match is None:
360
+ return None
361
+ token = match.group(1)
362
+ mapping = {"one": 1, "two": 2, "three": 3, "1": 1, "2": 2, "3": 3}
363
+ return mapping.get(token)
364
+
365
+ def _answer_only_text(self, response: NativeResponse) -> str:
366
+ parts = [part.strip() for part in response.answer.split("|") if part.strip()]
367
+ if not parts:
368
+ return response.text.strip()
369
+ humanized = [self.surface._humanize(part) for part in parts]
370
+ if len(humanized) == 1:
371
+ return humanized[0]
372
+ return "; ".join(humanized)
373
+
374
+ def _bullet_text(self, response: NativeResponse) -> str:
375
+ parts = [part.strip() for part in response.answer.split("|") if part.strip()]
376
+ if parts:
377
+ return "\n".join(f"- {self.surface._sentence(self.surface._humanize(part)).strip()}" for part in parts)
378
+ sentences = self._split_sentences(response.text)
379
+ if not sentences:
380
+ sentences = [response.text.strip()]
381
+ return "\n".join(f"- {self.surface._sentence(sentence).strip()}" for sentence in sentences if sentence.strip())
382
+
383
+ def _reshape_to_sentence_target(self, response: NativeResponse, target: int) -> str:
384
+ parts = [part.strip() for part in response.answer.split("|") if part.strip()]
385
+ if parts:
386
+ part_sentences = [self.surface._sentence(self.surface._humanize(part)).strip() for part in parts]
387
+ if target == 1:
388
+ return self.surface._sentence("; ".join(self.surface._humanize(part) for part in parts)).strip()
389
+ if len(part_sentences) >= target:
390
+ return " ".join(part_sentences[:target])
391
+ if response.mode == "story":
392
+ story_sentences = [
393
+ self.surface._sentence(sentence).strip()
394
+ for sentence in (
395
+ self.surface._proof_line_to_sentence(step) for step in response.proof
396
+ )
397
+ if sentence
398
+ ]
399
+ if len(story_sentences) < target:
400
+ story_sentences.extend(self._split_sentences(response.text))
401
+ if len(story_sentences) >= target:
402
+ return " ".join(story_sentences[-target:])
403
+ if response.mode == "plan":
404
+ answer_sentence = self.surface._sentence(self.surface._humanize(response.answer)).strip()
405
+ support_candidates = [sentence for sentence in self._candidate_sentences(response) if self.surface._humanize(response.answer).lower() not in sentence.lower()]
406
+ if target == 1:
407
+ return answer_sentence
408
+ selected = [answer_sentence]
409
+ selected.extend(support_candidates[: max(target - 1, 0)])
410
+ while len(selected) < target:
411
+ selected.append(answer_sentence)
412
+ return " ".join(selected[:target])
413
+ candidates = self._candidate_sentences(response)
414
+ if not candidates:
415
+ return response.text.strip()
416
+ if target == 1:
417
+ return self.surface._sentence(" ".join(sentence.rstrip(".!?") for sentence in candidates[:2])).strip()
418
+ selected = candidates[:target]
419
+ while len(selected) < target:
420
+ selected.append(self.surface._sentence(self.surface._humanize(response.answer)).strip())
421
+ return " ".join(selected[:target])
422
+
423
+ def _candidate_sentences(self, response: NativeResponse) -> list[str]:
424
+ candidates: list[str] = []
425
+ seen: set[str] = set()
426
+ for text in [response.text, response.explanation, *response.reasoning]:
427
+ for sentence in self._split_sentences(text):
428
+ normalized = sentence.strip()
429
+ if normalized and normalized not in seen:
430
+ candidates.append(self.surface._sentence(normalized).strip())
431
+ seen.add(normalized)
432
+ if response.answer and response.answer != "<unknown>":
433
+ normalized_answer = self.surface._humanize(response.answer)
434
+ if normalized_answer not in seen:
435
+ candidates.append(self.surface._sentence(normalized_answer).strip())
436
+ return candidates
437
+
438
+ @staticmethod
439
+ def _split_sentences(text: str) -> list[str]:
440
+ return [piece.strip() for piece in re.split(r"(?<=[.!?])\s+", text.strip()) if piece.strip()]
441
+
442
+
443
  def inspect(self, text: str) -> list[dict[str, object]]:
444
  return self.codec.export_tokens(text)
445
 
 
476
  def _split_query_parts(query: str) -> list[str]:
477
  parts: list[str] = []
478
  for part in re.split(
479
+ r"(?:\?\s+|\?\s*$|(?:\s+and\s+also\s+)|(?:\s+also\s+)|(?:\s*;\s*)|(?:\s+then\s+)|(?:,\s+(?=what|where|who|how|why|which|solve))|(?:\s+and\s+(?=what|where|who|how|why|which|solve))|(?:\r?\n+))",
480
  query,
481
  ):
482
  cleaned = part.strip()
runtime/aethon/rfi_surface.py CHANGED
@@ -457,13 +457,16 @@ class GraphVerbalizer:
457
  return []
458
  concepts = self._unknown_query_concepts(query)
459
  query_kind = self._unknown_query_kind(query)
 
460
  supports: list[str] = []
461
  seen: set[str] = set()
462
  for concept in concepts[:2]:
463
  edges = [
464
  edge
465
  for edge in self.graph.iter_outgoing_edges(concept)
466
- if edge.is_active and self._edge_matches_unknown_query_kind(edge.relation, query_kind)
 
 
467
  ]
468
  edges.sort(key=lambda edge: (0 if edge.source_kind != "derived" else 1, -edge.edge_id))
469
  for edge in edges[:2]:
 
457
  return []
458
  concepts = self._unknown_query_concepts(query)
459
  query_kind = self._unknown_query_kind(query)
460
+ lowered_query = query.lower()
461
  supports: list[str] = []
462
  seen: set[str] = set()
463
  for concept in concepts[:2]:
464
  edges = [
465
  edge
466
  for edge in self.graph.iter_outgoing_edges(concept)
467
+ if edge.is_active and (
468
+ "what changed about " in lowered_query or self._edge_matches_unknown_query_kind(edge.relation, query_kind)
469
+ )
470
  ]
471
  edges.sort(key=lambda edge: (0 if edge.source_kind != "derived" else 1, -edge.edge_id))
472
  for edge in edges[:2]: