Skip to content

Commit dc984fd

Browse files
authored
Merge pull request #14 from Aaryanverma/bug-fixes
Refactor functionality, TUI, and enhance search features
2 parents 2bcc7a0 + 7e7fd55 commit dc984fd

14 files changed

Lines changed: 1389 additions & 401 deletions

graybox/adaptive_compressor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def compress_context(
4444
if not model_context_window:
4545
model_context_window = _FALLBACK_MODEL_CONTEXT_WINDOW
4646

47-
available_context = model_context_window - max_tokens - 1000 # safety buffer
47+
available_context = min(max_tokens, model_context_window // 2)
4848
if estimate_tokens(context) <= available_context:
4949
return context
5050

graybox/cli.py

Lines changed: 42 additions & 206 deletions
Large diffs are not rendered by default.

graybox/curate.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ def merge_pages(cfg: Config, primary_ref: str, secondary_ref: str, dry_run: bool
110110
cfg, old_ref=secondary.ref, new_ref=merged.ref, exclude_refs={merged.ref}
111111
)
112112
_maybe_record(cfg, merged, f"wiki: merge {secondary_ref} into {primary_ref}")
113-
ensure_indexed(cfg, merged, AIService(cfg))
113+
if cfg.embeddings.enabled:
114+
ensure_indexed(cfg, merged, AIService(cfg))
114115
# Drop old secondary from embedding index
115116
idx = _get_index(cfg)
116117
if idx:

graybox/embedding_index.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def _get_index(cfg: Config) -> Optional[EmbeddingIndex]:
153153
return _EMBEDDING_CACHE[key]
154154

155155

156-
def ensure_indexed(cfg: Config, page: Page, llm) -> bool:
156+
def ensure_indexed(cfg: Config, page: Page, embed_model) -> bool:
157157
"""Index a page if embeddings are enabled and it needs reindexing.
158158
Returns True if indexed successfully.
159159
"""
@@ -164,7 +164,7 @@ def ensure_indexed(cfg: Config, page: Page, llm) -> bool:
164164
return True
165165
try:
166166
blob = idx._search_blob(page)
167-
result = llm.embedding_call(blob)
167+
result = embed_model.embedding_call(blob)
168168
if result and result.get("embedding"):
169169
idx.index_page(page, result["embedding"])
170170
return True

graybox/index.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ def _load_manifest(cfg: Config) -> dict:
5656
return _MANIFEST[key]
5757

5858

59-
def _save_manifest(cfg: Config) -> None:
60-
p = _manifest_path(cfg)
61-
p.write_text(json.dumps(_MANIFEST.get(_cache_key(cfg), {}), indent=2), encoding="utf-8")
59+
# def _save_manifest(cfg: Config) -> None:
60+
# p = _manifest_path(cfg)
61+
# p.write_text(json.dumps(_MANIFEST.get(_cache_key(cfg), {}), indent=2), encoding="utf-8")
6262

6363

6464
def _page_cache(cfg: Config) -> dict[str, tuple[float, Page]]:

graybox/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"task",
2121
"decision",
2222
"action",
23+
"event",
2324
"journal",
2425
)
2526

@@ -34,6 +35,7 @@
3435
"task": "tasks",
3536
"decision": "decisions",
3637
"action": "actions",
38+
"event": "events",
3739
"journal": "journal",
3840
}
3941

graybox/organizer.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def _gather_existing_context(cfg: Config, item_content: str, top_k: int = 10) ->
150150
LLM to reconcile against, not a final grounded answer - false
151151
positives here just mean an extra reference line, not a wrong claim.
152152
"""
153-
wiki_hits, _ = search_all(cfg, item_content, top_k=top_k, min_score=0.3)
153+
wiki_hits, _ = search_all(cfg, item_content, top_k=top_k, wiki_min_score=0.3)
154154
return _format_existing_context(wiki_hits)
155155

156156
def process_item(cfg: Config, llm: AIService, item_id: str, item_content: str,
@@ -259,6 +259,31 @@ def touch(page: Page, new: bool = False) -> Page:
259259
_backlink(owner_page, page.ref)
260260
touch(owner_page)
261261

262+
for act in data.get("actions", []):
263+
title = act.get("title", "").strip()
264+
if not title:
265+
continue
266+
page, new = _get_or_create_page(cfg, "action", title, [], "")
267+
page.status = (act.get("status", "open") or "open").replace("_", "-").strip()
268+
owner = act.get("owner", "")
269+
due = act.get("due", "")
270+
if owner:
271+
page.owner = owner
272+
if due:
273+
page.due = due
274+
detail = f"Action: {title}"
275+
if owner:
276+
detail += f" (owner: {owner})"
277+
if due:
278+
detail += f" (due: {due})"
279+
_append_note(page, detail, item_id, raw=item_content)
280+
touch(page, new)
281+
if owner and owner.lower() in name_to_page:
282+
owner_page = name_to_page[owner.lower()]
283+
_link(page, owner_page.ref)
284+
_backlink(owner_page, page.ref)
285+
touch(owner_page)
286+
262287
for dec in data.get("decisions", []):
263288
title = dec.get("title", "").strip()
264289
if not title:
@@ -308,6 +333,23 @@ def touch(page: Page, new: bool = False) -> Page:
308333
_link(meeting_page, other.ref)
309334
_backlink(other, meeting_page.ref)
310335

336+
for evt in data.get("events", []):
337+
title = evt.get("title", "").strip()
338+
if not title:
339+
continue
340+
page, new = _get_or_create_page(cfg, "event", title, [], "")
341+
if evt.get("date"):
342+
page.date = evt["date"]
343+
344+
description = evt.get("description", "") or f"Event: {title}"
345+
location = evt.get("location", "")
346+
detail = description
347+
if location:
348+
detail += f" (location: {location})"
349+
350+
_append_note(page, detail, item_id, raw=item_content)
351+
touch(page, new)
352+
311353
if dry_run:
312354
return [f"{ref} ({'new' if is_new.get(ref) else 'updated'})" for ref in touched]
313355

graybox/prompts.py

Lines changed: 114 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,139 @@
1-
ORGANIZER_SYSTEM = """You are an information-extraction engine for a personal knowledge base.
1+
ORGANIZER_SYSTEM = """You are Gray Box's Organizer.
22
3-
You read one raw note at a time and extract structured facts from it.
3+
Your job is to convert one raw note into structured knowledge.
44
5-
You never invent information that is not stated or clearly implied in the note.
5+
Extract SALIENT entities with HIGH recall while remaining completely faithful to the note.
6+
Never invent facts.
7+
Never return all-empty arrays for a non-empty note; if the note is vague, fall back to extracting a "topic" entity capturing the note's subject.
8+
When uncertain about an entity's type, prefer "topic" rather than omitting it.
69
7-
You respond with STRICT JSON only:
8-
no markdown fences, no commentary, no trailing text before or after the JSON object.
10+
Return STRICT JSON only.
11+
Do not output markdown.
12+
Do not output explanations.
13+
Do not output anything except one JSON object.
914
"""
1015

1116
ORGANIZER_PROMPT_TMPL = """Extract structured knowledge from the note below.
1217
13-
Your job is to keep the knowledge base synchronized with reality.
18+
YOUR JOB: Convert ONE raw note into structured knowledge. You MUST always
19+
return at least one item for any non-empty note. If the note is short,
20+
vague, or contains no clearly-named subject, extract a "topic" entity
21+
whose name is the note's central subject and whose summary is the note
22+
itself (lightly cleaned). Never return all-empty arrays for a non-empty note.
23+
24+
PRECISION vs RECALL:
25+
- Extract every SALIENT item the note is actually about (HIGH recall).
26+
- Do NOT extract peripheral mentions: words that merely appear in the
27+
note but are not the subject of any factual statement.
28+
- A noun is salient only if the note says something ABOUT it
29+
(a fact, a status, an action, a relationship, an opinion).
30+
- Verbs, adjectives, dates, and time expressions are NOT entities by themselves.
31+
- "Clearly implied" means a reasonable reader would name the same thing;
32+
it does NOT mean "this word could plausibly be an entity."
33+
34+
WHEN UNCERTAIN about an entity's type, prefer "topic" over omitting it.
35+
A vague note about "the meeting" with no other detail should still yield
36+
a topic entity named after the meeting subject.
37+
38+
FEW-SHOT EXAMPLES:
39+
40+
Note: "Standup tomorrow at 9am in room 4."
41+
Output:
42+
{{
43+
"entities": [],
44+
"relations": [],
45+
"tasks": [],
46+
"actions": [],
47+
"decisions": [],
48+
"meetings": [],
49+
"events": [
50+
{{"title": "Standup", "date": "", "description": "Standup scheduled for tomorrow at 9am in room 4.", "location": "room 4"}}
51+
]
52+
}}
53+
54+
Note: "Read an article about Rust ownership; might be relevant to Project Atlas."
55+
Output:
56+
{{
57+
"entities": [
58+
{{"type": "technology", "name": "Rust", "aliases": [], "summary": "Programming language; user read an article about its ownership model.", "status": "", "owner": "", "due": "", "date": "", "attendees": [], "tags": ["ownership"]}},
59+
{{"type": "project", "name": "Project Atlas", "aliases": [], "summary": "Project that may benefit from Rust ownership patterns.", "status": "", "owner": "", "due": "", "date": "", "attendees": [], "tags": []}}
60+
],
61+
"relations": [
62+
{{"a": "Rust", "b": "Project Atlas", "note": "Rust ownership model may be relevant to Project Atlas."}}
63+
],
64+
"tasks": [],
65+
"actions": [],
66+
"decisions": [],
67+
"meetings": [],
68+
"events": []
69+
}}
70+
71+
Note: "Felt tired today." <- minimal/vague note
72+
Output:
73+
{{
74+
"entities": [
75+
{{"type": "topic", "name": "Personal Log", "aliases": [], "summary": "User felt tired today.", "status": "", "owner": "", "due": "", "date": "", "attendees": [], "tags": ["journal"]}}
76+
],
77+
"relations": [], "tasks": [], "actions": [], "decisions": [], "meetings": [], "events": []
78+
}}
79+
80+
ANTI-EXAMPLES (do NOT do this):
81+
- Note "Discussed pricing with John." -> Do NOT extract "pricing" as an entity. DO extract John (person) and optionally a topic "Pricing discussion".
82+
- Note "Sent the email." -> Extract at most a topic "Email - sent" IF the note has no other subject. Do NOT extract "email" as a technology entity in this case.
1483
1584
A note may do one or more of the following:
1685
- introduce new entities
1786
- update the current state of existing entities
1887
- create relationships between entities
1988
- create or update tasks
89+
- create or update actions
2090
- create or update decisions
2191
- create or update meetings
92+
- create or update events
2293
2394
The knowledge base has two layers:
2495
2596
1. Current state
26-
- summary
27-
- status
28-
- owner
29-
- due
30-
- date
31-
- attendees
32-
- aliases
33-
- tags
97+
- summary, status, owner, due, date, attendees, aliases, tags
3498
3599
2. History
36100
- every extracted item should also be appended as a historical note
37101
38102
When a note changes the current state of an entity, return the NEW current state
39103
rather than the old one.
40104
41-
Examples:
42-
- "Project Atlas has been archived."
43-
-> status = "done" or "archived" only if clearly supported by the note
44-
- "DB cutover task has been reviewed and deployed."
45-
-> status = "done"
46-
- "Architecture meeting moved to Friday."
47-
-> date = "YYYY-MM-DD" if the date can be resolved from the note
48-
- "John is now Engineering Manager."
49-
-> summary reflects the new role
50-
51105
Use hyphenated status values when relevant:
52106
open, in-progress, blocked, done, cancelled
53107
54108
Existing pages already in the knowledge base that MAY relate to this note
55109
(any type - project, person, meeting, technology, company, topic, action,
56-
task, or decision). This is provided so you can RECONCILE state changes
110+
task, event, or decision). This is provided so you can RECONCILE state changes
57111
against what already exists, instead of creating a disconnected duplicate:
58112
59113
{existing_context}
60114
61-
UNIVERSAL RECONCILIATION RULE (applies to every category below, not just one):
62-
- Before extracting ANY entity, task, decision, or meeting, check whether it
63-
refers to the SAME underlying thing as one of the existing pages listed
64-
above - even if the note's wording, category, or phrasing differs from
65-
how that page was originally tracked. The same underlying work or topic
66-
can legitimately be tracked as a "task" in one note and described in
67-
"project"/"topic"/other entity language in another - these are not
68-
different things just because the note's phrasing differs.
69-
- If a match exists, reuse that EXISTING page's exact title (and reference
70-
its type) in your output instead of inventing a new title. Do not
71-
create a near-duplicate with slightly different wording.
115+
UNIVERSAL RECONCILIATION RULE:
116+
- Before extracting ANY entity, task, action, event, decision, or meeting,
117+
check whether it refers to the SAME underlying thing as one of the existing
118+
pages listed above.
119+
- If a HIGH-CONFIDENCE match exists (same proper noun, same date+subject, or
120+
one is an exact alias of the other), reuse that EXISTING page's exact title
121+
and reference its type in your output instead of inventing a new title.
122+
- If a match is only "thematically related" or a loose conceptual fit, DO NOT
123+
force a match — extract the new item as-is with its own unique title.
72124
- If the note describes a status change (reviewed, completed, deployed,
73125
blocked, cancelled, decided, rescheduled, etc.) for something that
74-
matches an existing page - REGARDLESS OF WHICH CATEGORY THAT EXISTING
75-
PAGE IS IN - emit an update for that exact category AND title. If the
76-
same real-world item is tracked as both an existing task and referenced
77-
as a project/entity, update BOTH using their existing exact titles so
78-
neither one goes stale.
79-
- This rule applies symmetrically across entities, tasks, decisions, and
80-
meetings: a note phrased as a decision can update an existing task; a
81-
note phrased as a project update can update an existing task; a note
82-
phrased as a task update can update an existing decision or meeting
83-
outcome; etc. Match on underlying meaning, not on which list something
84-
was originally extracted into.
126+
HIGH-CONFIDENCE matches an existing page, emit an update for that exact
127+
category AND title. If the same real-world item is tracked as both an
128+
existing task and referenced as a project/entity, update BOTH using their
129+
existing exact titles so neither one goes stale.
85130
- Never invent a new title for something that already exists above.
86131
87132
Return a JSON object strictly with the following structure:
88133
{{
89134
"entities": [
90135
{{
91-
"type": "project|person|meeting|technology|company|topic|action",
136+
"type": "project|person|meeting|technology|company|topic|action|event",
92137
"name": "canonical name",
93138
"aliases": ["other names used"],
94139
"summary": "one short sentence describing the entity in its current state",
@@ -115,6 +160,14 @@
115160
"status": "open|in-progress|blocked|done|cancelled"
116161
}}
117162
],
163+
"actions": [
164+
{{
165+
"title": "short imperative action title",
166+
"owner": "person or empty string",
167+
"due": "date or empty string",
168+
"status": "open|in-progress|blocked|done|cancelled"
169+
}}
170+
],
118171
"decisions": [
119172
{{
120173
"title": "short decision title",
@@ -129,11 +182,20 @@
129182
"attendees": ["person names present in the note"],
130183
"agenda": "one to two sentence summary of what the meeting covered"
131184
}}
185+
],
186+
"events": [
187+
{{
188+
"title": "short event title",
189+
"date": "YYYY-MM-DD or empty string",
190+
"description": "short summary of the event",
191+
"location": "event location or empty string"
192+
}}
132193
]
133194
}}
134195
135196
Rules:
136-
- Only include entities/tasks/decisions/meetings actually present or clearly implied in the note.
197+
- Only include SALIENT items actually present or clearly implied in the note.
198+
- If an item is an action or an event, put it in the `actions` or `events` array. Do NOT duplicate them inside the `entities` array.
137199
- Keep summaries and descriptions concise (1-2 sentences).
138200
- If a category is empty, return an empty list for it.
139201
- Do not wrap the JSON in markdown code fences.
@@ -144,7 +206,6 @@
144206
NOTE:
145207
---
146208
{note}
147-
---
148209
"""
149210

150211
RETRIEVAL_SYSTEM = """You are Gray Box's knowledge retrieval assistant.
@@ -347,11 +408,9 @@
347408
appeared under. Never move a fact to a different tag, never merge two
348409
tags' content under one tag, and never drop a Source Tag line while
349410
keeping content from that block.
350-
- If a block's content is fully removed because it is pure repetition of
351-
another block, you may drop the block entirely (tag and all) — do not
352-
leave a Source Tag with no content, and do not leave content with no
353-
Source Tag.
354-
411+
- Only drop a block if it is BYTE-FOR-BYTE identical to another block.
412+
"Similar Topic" is not repetition - two pages about the same project
413+
may carry different notes/information. When in doubt, keep the block.
355414
Return only the compressed context."""
356415

357416
HISTORY_COMPRESSION_PROMPT = """You are a highly efficient text summarizer. Summarize the following conversation history.

0 commit comments

Comments
 (0)