Skip to content

Commit 4f1d332

Browse files
committed
feat: add concepts plan and update prompt templates
Add _CONCEPTS_PLAN_USER (create/update/related JSON structure) and _CONCEPT_UPDATE_USER templates; add TestParseConceptsPlan tests.
1 parent 8640681 commit 4f1d332

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

openkb/agent/compiler.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,33 @@
6969
Return ONLY valid JSON array, no fences, no explanation.
7070
"""
7171

72+
_CONCEPTS_PLAN_USER = """\
73+
Based on the summary above, decide how to update the wiki's concept pages.
74+
75+
Existing concept pages:
76+
{concept_briefs}
77+
78+
Return a JSON object with three keys:
79+
80+
1. "create" — new concepts not covered by any existing page. Array of objects:
81+
{{"name": "concept-slug", "title": "Human-Readable Title"}}
82+
83+
2. "update" — existing concepts that have significant new information from \
84+
this document worth integrating. Array of objects:
85+
{{"name": "existing-slug", "title": "Existing Title"}}
86+
87+
3. "related" — existing concepts tangentially related to this document but \
88+
not needing content changes, just a cross-reference link. Array of slug strings.
89+
90+
Rules:
91+
- For the first few documents, create 2-3 foundational concepts at most.
92+
- Do NOT create a concept that overlaps with an existing one — use "update".
93+
- Do NOT create concepts that are just the document topic itself.
94+
- "related" is for lightweight cross-linking only, no content rewrite needed.
95+
96+
Return ONLY valid JSON, no fences, no explanation.
97+
"""
98+
7299
_CONCEPT_PAGE_USER = """\
73100
Write the concept page for: {title}
74101
@@ -81,6 +108,20 @@
81108
- [[wikilinks]] to related concepts and [[summaries/{doc_name}]]
82109
"""
83110

111+
_CONCEPT_UPDATE_USER = """\
112+
Update the concept page for: {title}
113+
114+
Current content of this page:
115+
{existing_content}
116+
117+
New information from document "{doc_name}" (summarized above) should be \
118+
integrated into this page. Rewrite the full page incorporating the new \
119+
information naturally — do not just append. Maintain existing \
120+
[[wikilinks]] and add new ones where appropriate.
121+
122+
Return ONLY the Markdown content (no frontmatter, no code fences).
123+
"""
124+
84125
_LONG_DOC_SUMMARY_USER = """\
85126
This is a PageIndex summary for long document "{doc_name}" (doc_id: {doc_id}):
86127
@@ -296,6 +337,36 @@ def _write_concept(wiki_dir: Path, name: str, content: str, source_file: str, is
296337
path.write_text(frontmatter + content, encoding="utf-8")
297338

298339

340+
def _add_related_link(wiki_dir: Path, concept_slug: str, doc_name: str, source_file: str) -> None:
341+
"""Add a cross-reference link to an existing concept page (no LLM call)."""
342+
concepts_dir = wiki_dir / "concepts"
343+
path = concepts_dir / f"{concept_slug}.md"
344+
if not path.exists():
345+
return
346+
347+
text = path.read_text(encoding="utf-8")
348+
link = f"[[summaries/{doc_name}]]"
349+
if link in text:
350+
return
351+
352+
# Update sources in frontmatter
353+
if source_file not in text:
354+
if text.startswith("---"):
355+
end = text.index("---", 3)
356+
fm = text[:end + 3]
357+
body = text[end + 3:]
358+
if "sources:" in fm:
359+
fm = fm.replace("sources: [", f"sources: [{source_file}, ")
360+
else:
361+
fm = fm.replace("---\n", f"---\nsources: [{source_file}]\n", 1)
362+
text = fm + body
363+
else:
364+
text = f"---\nsources: [{source_file}]\n---\n\n" + text
365+
366+
text += f"\n\nSee also: {link}"
367+
path.write_text(text, encoding="utf-8")
368+
369+
299370
def _update_index(wiki_dir: Path, doc_name: str, concept_names: list[str]) -> None:
300371
"""Append document and concept entries to index.md."""
301372
index_path = wiki_dir / "index.md"

tests/test_compiler.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
_update_index,
1717
_read_wiki_context,
1818
_read_concept_briefs,
19+
_add_related_link,
1920
)
2021

2122

@@ -32,6 +33,31 @@ def test_invalid_json(self):
3233
_parse_json("not json")
3334

3435

36+
class TestParseConceptsPlan:
37+
def test_dict_format(self):
38+
text = json.dumps({
39+
"create": [{"name": "foo", "title": "Foo"}],
40+
"update": [{"name": "bar", "title": "Bar"}],
41+
"related": ["baz"],
42+
})
43+
parsed = _parse_json(text)
44+
assert isinstance(parsed, dict)
45+
assert len(parsed["create"]) == 1
46+
assert len(parsed["update"]) == 1
47+
assert parsed["related"] == ["baz"]
48+
49+
def test_fallback_list_format(self):
50+
text = json.dumps([{"name": "foo", "title": "Foo"}])
51+
parsed = _parse_json(text)
52+
assert isinstance(parsed, list)
53+
54+
def test_fenced_dict(self):
55+
text = '```json\n{"create": [], "update": [], "related": []}\n```'
56+
parsed = _parse_json(text)
57+
assert isinstance(parsed, dict)
58+
assert parsed["create"] == []
59+
60+
3561
class TestWriteSummary:
3662
def test_writes_with_frontmatter(self, tmp_path):
3763
wiki = tmp_path / "wiki"
@@ -180,6 +206,39 @@ def test_sorted_alphabetically(self, tmp_path):
180206
assert slugs == ["apple", "mango", "zebra"]
181207

182208

209+
class TestAddRelatedLink:
210+
def test_adds_see_also_link(self, tmp_path):
211+
wiki = tmp_path / "wiki"
212+
concepts = wiki / "concepts"
213+
concepts.mkdir(parents=True)
214+
(concepts / "attention.md").write_text(
215+
"---\nsources: [paper1.pdf]\n---\n\n# Attention\n\nSome content.",
216+
encoding="utf-8",
217+
)
218+
_add_related_link(wiki, "attention", "new-doc", "paper2.pdf")
219+
text = (concepts / "attention.md").read_text()
220+
assert "[[summaries/new-doc]]" in text
221+
assert "paper2.pdf" in text
222+
223+
def test_skips_if_already_linked(self, tmp_path):
224+
wiki = tmp_path / "wiki"
225+
concepts = wiki / "concepts"
226+
concepts.mkdir(parents=True)
227+
(concepts / "attention.md").write_text(
228+
"---\nsources: [paper1.pdf]\n---\n\n# Attention\n\nSee also: [[summaries/new-doc]]",
229+
encoding="utf-8",
230+
)
231+
_add_related_link(wiki, "attention", "new-doc", "paper1.pdf")
232+
text = (concepts / "attention.md").read_text()
233+
assert text.count("[[summaries/new-doc]]") == 1
234+
235+
def test_skips_if_file_missing(self, tmp_path):
236+
wiki = tmp_path / "wiki"
237+
wiki.mkdir()
238+
# Should not raise
239+
_add_related_link(wiki, "nonexistent", "doc", "file.pdf")
240+
241+
183242
def _mock_completion(responses: list[str]):
184243
"""Create a mock for litellm.completion that returns responses in order."""
185244
call_count = {"n": 0}

0 commit comments

Comments
 (0)