feat: local LLM multi-model, session search, cron proactive types, notifications, docs overhaul
Local LLM:
- user_settings.py: per-user hosts/models config (local_llm.json)
- routers/local_llm.py + static/local_llm.html: dedicated settings page
- llm_client.py: local OpenAI-compatible backend via httpx
- config.py: LOCAL_API_URL/KEY/MODEL + per-backend timeouts
- Active model shown near backend toggle (amber hint text)
Memory distillation:
- memory_distiller.py: DISTILL_BACKEND_MID/LONG .env overrides
- scheduler.py + notification.py: notify NC Talk after mid/long distill
- notification.py: outbound channel abstraction (NC Talk, extensible)
Session search:
- routers/files.py: GET /sessions/search?q= with excerpts grouped by date
- static/index.html + app.js: search UI in file sidebar with highlight
- _esc() helper to prevent XSS in search results
Proactive cron:
- cron_runner.py: new job types — message (send directly) and brief (LLM + send)
- Both support optional per-job channel override
Channels:
- routers/nextcloud_talk.py: consolidated using notification._send_nct_message()
- routers/auth.py: local backend status in /auth/status
- routers/chat.py: /backend returns {primary, fallback, local_model} object
UI / UX:
- Copy button for user messages (matching assistant)
- Autocomplete disabled on sensitive form fields
- settings.html: local model section replaced with link to /settings/local
Docs overhaul:
- MASTER.md hub + ARCH__SYSTEM/BACKENDS/PERSONA/CHANNELS/FUTURE.md
- ARCH__Intelligence_Layer.md replaced with redirect table
- CORTEX.md trimmed to vision only; README updated
- OPEN_WEBUI_API.md added to docs/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"""
|
||||
Read/write the Inara identity markdown files.
|
||||
Read/write Inara identity markdown files, and search past session logs.
|
||||
Only whitelisted filenames are accessible — no path traversal possible.
|
||||
"""
|
||||
import re
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from persona import persona_path, set_context, validate as validate_persona
|
||||
@@ -47,10 +48,12 @@ async def list_files(
|
||||
files = []
|
||||
for name in sorted(ALLOWED):
|
||||
p = persona_dir / name
|
||||
st = p.stat() if p.exists() else None
|
||||
files.append({
|
||||
"name": name,
|
||||
"exists": p.exists(),
|
||||
"size": p.stat().st_size if p.exists() else 0,
|
||||
"size": st.st_size if st else 0,
|
||||
"modified": st.st_mtime if st else None,
|
||||
})
|
||||
return {"files": files}
|
||||
|
||||
@@ -83,3 +86,59 @@ async def save_file(
|
||||
p = _path(filename)
|
||||
p.write_text(req.content)
|
||||
return {"ok": True, "name": filename, "size": len(req.content)}
|
||||
|
||||
|
||||
# ── Session search ────────────────────────────────────────────────────────────
|
||||
|
||||
_CONTEXT_CHARS = 120 # chars of context to include around each match
|
||||
|
||||
|
||||
@router.get("/sessions/search")
|
||||
async def search_sessions(
|
||||
q: str = Query(..., min_length=2),
|
||||
user: str = Query("scott"),
|
||||
persona: str = Query("inara"),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
) -> dict:
|
||||
"""Full-text search across past session logs.
|
||||
|
||||
Returns up to `limit` matches, newest sessions first.
|
||||
Each match includes a short excerpt (120 chars before/after) for context.
|
||||
"""
|
||||
_resolve(user, persona)
|
||||
sessions_dir = persona_path() / "sessions"
|
||||
if not sessions_dir.exists():
|
||||
return {"query": q, "matches": [], "total_files_searched": 0}
|
||||
|
||||
pattern = re.compile(re.escape(q), re.IGNORECASE)
|
||||
session_files = sorted(sessions_dir.glob("*.md"), reverse=True) # newest first
|
||||
|
||||
matches = []
|
||||
for sf in session_files:
|
||||
if len(matches) >= limit:
|
||||
break
|
||||
try:
|
||||
text = sf.read_text()
|
||||
except OSError:
|
||||
continue
|
||||
for m in pattern.finditer(text):
|
||||
if len(matches) >= limit:
|
||||
break
|
||||
start = max(0, m.start() - _CONTEXT_CHARS)
|
||||
end = min(len(text), m.end() + _CONTEXT_CHARS)
|
||||
excerpt = text[start:end].strip()
|
||||
# Prefix with ellipsis if we truncated the left side
|
||||
if start > 0:
|
||||
excerpt = "…" + excerpt
|
||||
if end < len(text):
|
||||
excerpt = excerpt + "…"
|
||||
matches.append({
|
||||
"date": sf.stem, # YYYY-MM-DD
|
||||
"excerpt": excerpt,
|
||||
})
|
||||
|
||||
return {
|
||||
"query": q,
|
||||
"matches": matches,
|
||||
"total_files_searched": len(session_files),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user