Skip to content

Commit d2c0fa8

Browse files
author
Jarvis
committed
feat: multi-provider session summarization with smart credential resolution
- Replace hard-coded Anthropic dependency with provider-agnostic flow - Support both Anthropic and OpenAI-compatible endpoints for session summaries - Add smart credential resolution: Hermes credential pool → Codex OAuth → env → .env - Read active provider from Hermes config.yaml for optimal routing - Bump version to v1.1.0
1 parent 0776b63 commit d2c0fa8

File tree

2 files changed

+98
-9
lines changed

2 files changed

+98
-9
lines changed

engram/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Engram — Jarvis persistent memory system."""
22

3-
__version__ = "1.0.0"
3+
__version__ = "1.1.0"
44

55
from .provider import EngramMemoryProvider
66

engram/provider.py

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
226226

227227
def _save_session_async(self, messages: List[Dict[str, Any]]) -> None:
228228
try:
229-
import anthropic, re
229+
import re, requests
230230

231231
turns = []
232232
for m in messages:
@@ -243,7 +243,8 @@ def _save_session_async(self, messages: List[Dict[str, Any]]) -> None:
243243
return
244244

245245
conversation = "\n\n".join(turns[-20:])
246-
client = anthropic.Anthropic(api_key=api_key)
246+
provider = self._cfg.get("llm_provider", "openai")
247+
model = self._cfg.get("llm_model", "gpt-5.4-mini")
247248
prompt = (
248249
"Summarize this conversation in 2 sentences max. "
249250
"Then list any unresolved threads or follow-up items as a bullet list (max 5). "
@@ -253,12 +254,34 @@ def _save_session_async(self, messages: List[Dict[str, Any]]) -> None:
253254
f"CONVERSATION:\n{conversation}"
254255
)
255256

256-
msg = client.messages.create(
257-
model=self._cfg.get("llm_model", "claude-haiku-4-5"),
258-
max_tokens=500,
259-
messages=[{"role": "user", "content": prompt}],
260-
)
261-
raw = msg.content[0].text.strip()
257+
if provider == "anthropic":
258+
import anthropic
259+
client = anthropic.Anthropic(api_key=api_key)
260+
msg = client.messages.create(
261+
model=model,
262+
max_tokens=500,
263+
messages=[{"role": "user", "content": prompt}],
264+
)
265+
raw = msg.content[0].text.strip()
266+
else:
267+
# OpenAI-compatible (uses whatever base_url the Hermes provider exposes)
268+
base_url = getattr(self, "_api_base_url", None) or "https://api.openai.com/v1"
269+
resp = requests.post(
270+
f"{base_url.rstrip('/')}/chat/completions",
271+
headers={
272+
"Authorization": f"Bearer {api_key}",
273+
"Content-Type": "application/json",
274+
},
275+
json={
276+
"model": model,
277+
"max_tokens": 500,
278+
"messages": [{"role": "user", "content": prompt}],
279+
},
280+
timeout=30,
281+
)
282+
resp.raise_for_status()
283+
raw = resp.json()["choices"][0]["message"]["content"].strip()
284+
262285
raw = re.sub(r"^```(?:json)?\s*", "", raw)
263286
raw = re.sub(r"\s*```$", "", raw)
264287
data = json.loads(raw)
@@ -303,9 +326,75 @@ def _regenerate_briefing(self) -> None:
303326
logger.debug("Engram: briefing regen failed: %s", e)
304327

305328
def _get_api_key(self) -> Optional[str]:
329+
"""Resolve an API key and base URL for session summarization.
330+
331+
Priority:
332+
1. ANTHROPIC_API_KEY env var (for Anthropic provider)
333+
2. Hermes credential pool (reads active provider from config.yaml)
334+
3. Codex OAuth access token from ~/.codex/auth.json
335+
4. .env file fallback
336+
337+
Sets self._api_base_url for the resolved provider.
338+
"""
339+
provider = self._cfg.get("llm_provider", "hermes")
340+
self._api_base_url = None
341+
342+
# Read active provider from Hermes config
343+
hermes_cfg_path = Path("~/.hermes/config.yaml").expanduser()
344+
hermes_provider = None
345+
hermes_base_url = None
346+
if hermes_cfg_path.exists():
347+
try:
348+
import yaml
349+
cfg = yaml.safe_load(hermes_cfg_path.read_text()) or {}
350+
hermes_provider = cfg.get("model", {}).get("provider", "")
351+
hermes_base_url = cfg.get("model", {}).get("base_url", "")
352+
except Exception:
353+
pass
354+
355+
if provider != "anthropic":
356+
# Try Hermes credential pool — match active provider first
357+
auth_path = Path("~/.hermes/auth.json").expanduser()
358+
if auth_path.exists():
359+
try:
360+
data = json.loads(auth_path.read_text())
361+
pool = data.get("credential_pool", {})
362+
# Try the active Hermes provider first
363+
if hermes_provider and hermes_provider in pool:
364+
for creds in pool[hermes_provider]:
365+
token = creds.get("access_token", "")
366+
if token and len(token) > 10:
367+
self._api_base_url = creds.get("base_url") or hermes_base_url
368+
return token
369+
# Fallback: scan all providers
370+
for prov_name, creds_list in pool.items():
371+
if prov_name == "anthropic":
372+
continue
373+
for creds in creds_list:
374+
token = creds.get("access_token", "")
375+
if token and len(token) > 10:
376+
self._api_base_url = creds.get("base_url") or hermes_base_url
377+
return token
378+
except Exception:
379+
pass
380+
381+
# Codex OAuth token
382+
codex_path = Path("~/.codex/auth.json").expanduser()
383+
if codex_path.exists():
384+
try:
385+
data = json.loads(codex_path.read_text())
386+
token = data.get("accessToken", "")
387+
if token and len(token) > 10:
388+
return token
389+
except Exception:
390+
pass
391+
392+
# Anthropic env var
306393
key = os.environ.get("ANTHROPIC_API_KEY", "")
307394
if key and key != "***":
308395
return key
396+
397+
# .env file
309398
env_path = Path("~/.hermes/.env").expanduser()
310399
if env_path.exists():
311400
for line in env_path.read_text().splitlines():

0 commit comments

Comments
 (0)