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:
+62
-3
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user