Skip to content

Commit 7fc20a0

Browse files
committed
updates
1 parent 2373f05 commit 7fc20a0

8 files changed

Lines changed: 275 additions & 137 deletions

File tree

.github/agents/Agent-Developer-Assistant/memory/MEMORY.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Agent Memory — Research Assistant
22

3+
## 2026-04-22 (Session resume after hard page refresh)
4+
5+
- **Root cause:** `activeSessionIdRef` and `researchConvIdRef` are in-memory `useRef`s — lost on hard refresh. `handleReconnected` was only called on WebSocket re-connections, not on the initial connection after a refresh. Backend fully supports replay but frontend never sent the `resume` message on initial connect.
6+
- **Fix — frontend only, no backend changes needed:**
7+
1. `useWebSocket.ts`: Added `onInitialConnect` callback option (fires on first `ws.onopen`; `onReconnected` fires on subsequent opens). Both stored in refs for stability.
8+
2. `App.tsx`: Both `useRef`s now pre-populated via `sessionStorage.getItem()` as initial value — safe because WS `onopen` is async and all React `useEffect`s complete before the network handshake.
9+
3. `SS_SESSION_ID` / `SS_CONV_ID` sessionStorage keys written on `session_created` + `handleSendMessage`, cleared on `report`, `research_stopped`, `plan_denied`, `error`.
10+
4. `handleInitialConnect`: reads sessionStorage and sends `{ type: 'resume', session_id }` if stored session exists.
11+
5. **Replay deduplication:** `replayCountRef = useRef(0)`. Set to `event_count` from `session_resumed`. In message loop, all non-control events decrement counter and set `isReplayedEvent = true` while > 0. `addMessageToConv`, `initFromPlan`, and `addSynthesisNode` guarded with `if (!isReplayedEvent)`. Graph state transitions are NOT guarded — they're idempotent.
12+
- **Key patterns:** `onInitialConnect` does NOT need the 150ms delay that `onReconnected` has — backend is already up on page load. `session_resumed` reconnect notification message is always added (not guarded) since it's a new message. `error` handler always clears sessionStorage since errors during a resume leave no valid session to return to.
13+
314
## 2026-04-15 (Knowledge Graph Schema Redesign — Typed Nodes, Relations, Claims, Documents)
415

516
- **Full rewrite of `long_term_memory.py`**: Replaced single `:Entity` node type with 7 typed Neo4j labels (`:Person`, `:Organization`, `:Technology`, `:Concept`, `:Event`, `:Location`, `:Metric`). Each type has its own uniqueness constraint, vector index, and type-specific properties (e.g., Person.affiliation, Technology.version, Metric.value+unit).

backend/api_server.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
# ── Config persistence ────────────────────────────────────────────────────────
4242
_SETTINGS_FILE = Path(
4343
os.path.join(
44-
os.path.dirname(os.path.abspath(__file__)),
44+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
4545
"configs",
4646
"model_settings.json",
4747
)
@@ -51,7 +51,6 @@
5151
def _load_persisted_settings() -> dict:
5252
"""Load overrides from the model_settings.json file if it exists."""
5353
if _SETTINGS_FILE.exists():
54-
logger.info("Loading persisted configurations...")
5554
try:
5655
return json.loads(_SETTINGS_FILE.read_text())
5756
except Exception as exc:

backend/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ class Config(BaseModel):
311311
)
312312
# Model used for heavy tasks (planning, synthesis, report generation).
313313
azure_heavy_model: str = Field(
314-
default_factory=lambda: os.getenv("AZURE_HEAVY_MODEL", "gpt-5.4")
314+
default_factory=lambda: os.getenv("AZURE_HEAVY_MODEL", "gpt-5.2")
315315
)
316316
# Model used for light tasks (search execution, analyst extraction).
317317
azure_light_model: str = Field(
@@ -325,7 +325,7 @@ class Config(BaseModel):
325325
# Default Bedrock model identifier.
326326
aws_model: str = Field(
327327
default_factory=lambda: os.getenv(
328-
"AWS_MODEL", "global.anthropic.claude-sonnet-4-6"
328+
"AWS_MODEL", "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
329329
)
330330
)
331331
# Model used for heavy tasks (planning, synthesis, report generation).

backend/long_term_memory.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
Persistent cross-session memory store backed by Neo4j, with an integrated
55
knowledge graph (GraphRAG) for capturing relationships between facts.
66
7-
Embedding is handled by the shared embeddings.py module (sentence-transformers +
7+
Replaces the standalone memory MCP server (mcp/memory/) by running the same
8+
persistence logic in-process alongside the FastAPI backend. Embedding is
9+
handled by the shared embeddings.py module (sentence-transformers +
810
ThreadPoolExecutor), so there is exactly one model instance and one thread-pool
911
in the process.
1012
@@ -783,8 +785,11 @@ class KnowledgeGraph:
783785
Relationship-aware knowledge graph stored natively in Neo4j.
784786
785787
Typed entity nodes (Person, Organization, Technology, Concept, Event,
786-
Location, Metric) has its own vector index for semantic search and
787-
type-specific properties. Free-form verb phrases are classified via keyword
788+
Location, Metric) replace the old generic Entity label. Each type has
789+
its own vector index for semantic search and type-specific properties.
790+
791+
Typed relationship labels (CAUSES, ENABLES, USES, etc.) replace the old
792+
catch-all RELATES_TO. Free-form verb phrases are classified via keyword
788793
matching into typed labels; unmatched phrases fall back to RELATES_TO.
789794
790795
Claim nodes store individual assertions linked to entities and documents.

backend/report_exporter.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,41 @@ def _get_reports_dir() -> Path:
4545
return Path(raw)
4646

4747

48-
def _sanitize_filename(text: str, max_len: int = 60) -> str:
49-
"""Turn arbitrary text into a safe filename fragment."""
50-
text = re.sub(r"[^\w\s-]", "", text).strip()
51-
text = re.sub(r"\s+", "_", text)
52-
return text[:max_len] if text else "report"
48+
# Common English stopwords that add no meaning to a filename.
49+
_STOPWORDS = frozenset(
50+
{
51+
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
52+
"of", "with", "by", "from", "up", "about", "into", "through", "is",
53+
"are", "was", "were", "be", "been", "being", "have", "has", "had",
54+
"do", "does", "did", "will", "would", "could", "should", "may",
55+
"might", "shall", "can", "what", "where", "when", "why", "how",
56+
"which", "who", "whom", "this", "that", "these", "those", "it",
57+
"its", "we", "they", "you", "i", "my", "our", "your", "their",
58+
"his", "her", "some", "any", "all", "each", "as", "if", "than",
59+
"so", "yet", "both", "just", "more", "most", "also", "me", "us",
60+
}
61+
)
62+
63+
64+
def _query_to_slug(query: str, max_words: int = 5) -> str:
65+
"""
66+
Produce a short, human-readable filename slug from a research query.
67+
68+
Strips punctuation and common stopwords, takes up to *max_words* of the
69+
remaining meaningful terms, title-cases them, and joins with underscores.
70+
71+
Example: "What are the geopolitical implications of rare earth scarcity?"
72+
→ "Geopolitical_Implications_Rare_Earth_Scarcity"
73+
"""
74+
# Remove non-alphanumeric characters (keep spaces and hyphens)
75+
cleaned = re.sub(r"[^\w\s-]", " ", query).strip()
76+
words = cleaned.split()
77+
meaningful = [
78+
w for w in words if w.lower() not in _STOPWORDS and len(w) > 1
79+
]
80+
chosen = meaningful[:max_words] if meaningful else words[:max_words]
81+
slug = "_".join(w.capitalize() for w in chosen)
82+
return slug if slug else "report"
5383

5484

5585
def _extract_title(document: str, query: str) -> str:
@@ -214,8 +244,7 @@ async def export_report_pdf(
214244
try:
215245
reports_dir = _get_reports_dir()
216246
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
217-
title = _extract_title(document, query)
218-
slug = _sanitize_filename(title)
247+
slug = _query_to_slug(query)
219248
sid_part = (session_id or "cli")[:8]
220249
filename = f"{date_str}_{sid_part}_{slug}.pdf"
221250
output_path = reports_dir / filename

backend/requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ boto3
88
neo4j
99
fastapi
1010
httpx
11-
fpdf2
1211
huggingface_hub
1312
mcp
1413
ollama

0 commit comments

Comments
 (0)