feat: workspaces, agentic action tools, local Whisper STT, vec recall

- Projects/workspaces: documents grouped into projects; chat RAG scopes to the
  active project. Switcher in the Documents page.
- Agentic action tools: web_search, fetch_url, and remember (first write tool),
  allowlist-gated per playbook.
- Local Whisper STT (faster-whisper, no torch): on-device dictation replacing
  the browser Web Speech API. POST /stt + GET /stt/status; browser fallback.
- Vector index extended to conversation recall (message_vectors), with the
  brute-force cosine scan kept as the fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-23 14:22:46 -05:00
co-authored by Claude Opus 4.8
parent f4aea78b55
commit ba6a4ac4e4
12 changed files with 586 additions and 53 deletions
+62 -3
View File
@@ -290,6 +290,7 @@ async def chat_stream_endpoint(payload: Dict[str, Any]):
message, get_ollama_manager().embed,
limit=app_settings.get("rag_top_k", 3),
min_score=app_settings.get("rag_min_score", 0.6),
project_id=app_settings.get("active_project", "") or None,
)
doc_titles: list = []
if doc_hits:
@@ -943,12 +944,70 @@ async def delete_playbook_endpoint(id: UUID):
raise HTTPException(status_code=500, detail=str(e))
# -------------------------
# Speech-to-text (local Whisper)
# -------------------------
from . import stt as _stt
@app.get("/stt/status")
async def stt_status():
return {"available": _stt.available()}
@app.post("/stt")
async def stt_transcribe(payload: Dict[str, Any] = Body(...)):
if not _stt.available():
raise HTTPException(status_code=503, detail="local STT (faster-whisper) not installed")
audio = payload.get("audio") or ""
if not audio:
raise HTTPException(status_code=400, detail="audio (base64) is required")
try:
text = await _asyncio.to_thread(_stt.transcribe_b64, audio)
except Exception as e:
raise HTTPException(status_code=500, detail=f"transcription failed: {e}")
return {"text": text}
# -------------------------
# Projects / workspaces
# -------------------------
def _active_project() -> str:
"""The active project id, or '' for the unscoped 'All' view."""
return store.get_settings().get("active_project", "") or ""
@app.get("/projects")
async def list_projects():
return {"projects": store.list_projects(), "active": _active_project()}
@app.post("/projects")
async def create_project(payload: Dict[str, Any] = Body(...)):
name = (payload.get("name") or "").strip()
if not name:
raise HTTPException(status_code=400, detail="name is required")
return store.create_project(name)
@app.delete("/projects/{project_id}")
async def delete_project(project_id: str):
if not store.delete_project(project_id):
raise HTTPException(status_code=404, detail="Not Found")
# If the deleted project was active, fall back to the "All" view.
if _active_project() == project_id:
store.update_settings({"active_project": ""})
return {"status": "deleted"}
# -------------------------
# Documents (RAG)
# -------------------------
@app.get("/documents")
async def list_documents():
return {"documents": store.list_documents()}
# Scope to the active project; "" (All) lists everything.
active = _active_project()
return {"documents": store.list_documents(active if active else None)}
@app.post("/documents")
@@ -957,7 +1016,7 @@ async def add_document(payload: Dict[str, Any] = Body(...)):
content = (payload.get("content") or "").strip()
if not title or not content:
raise HTTPException(status_code=400, detail="title and content are required")
result = await store.add_document(title, content, get_ollama_manager().embed)
result = await store.add_document(title, content, get_ollama_manager().embed, _active_project())
if result["chunks"] == 0:
raise HTTPException(status_code=400, detail="no text to index")
return result
@@ -1000,7 +1059,7 @@ async def upload_document(payload: Dict[str, Any] = Body(...)):
if not text:
raise HTTPException(status_code=400, detail="no extractable text in file")
title = _os.path.splitext(_os.path.basename(filename))[0] or filename
return await store.add_document(title, text, get_ollama_manager().embed)
return await store.add_document(title, text, get_ollama_manager().embed, _active_project())
@app.get("/documents/{doc_id}")