- Add app/api/endpoints/plex.py with Plex webhook receiver (fail-closed on missing secret) - Add app/services/plex/ with Plex API client and gardening content filter - Wire plex router into routes.py at /plex prefix - Add Plex + cf-video config vars to app/core/config.py and .env.example - Rename /knowledge/ingest endpoint to /knowledge/glean (pipeline verb alignment) - Update tests to use /knowledge/glean endpoint - Add python-multipart to environment.yml for form/webhook body parsing - Add dev/dev-stop/dev-logs commands to manage.sh (hot-reload without Docker) - Fix frontend/tsconfig.node.json to extend tsconfig.json (not tsconfig.node.json) - Add .dev-pids/ to .gitignore - Add frontend/package-lock.json
100 lines
2.7 KiB
Python
100 lines
2.7 KiB
Python
"""cf-harvest knowledge ingest + query endpoints."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
from app.core.config import settings
|
|
from app.db.store import Store
|
|
from app.models.schemas.knowledge import (
|
|
IngestResponse,
|
|
KnowledgeFactOut,
|
|
KnowledgeIngestRequest,
|
|
)
|
|
from app.services.knowledge.ingest import ingest
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _get_store() -> Store:
|
|
s = Store(settings.DB_PATH)
|
|
try:
|
|
yield s
|
|
finally:
|
|
s.close()
|
|
|
|
|
|
@router.post("/glean", response_model=IngestResponse)
|
|
async def ingest_knowledge(
|
|
body: KnowledgeIngestRequest, store: Store = Depends(_get_store)
|
|
):
|
|
return ingest(store.conn, body)
|
|
|
|
|
|
@router.get("", response_model=list[KnowledgeFactOut])
|
|
async def query_knowledge(
|
|
fact_type: Optional[str] = Query(None),
|
|
subject: Optional[str] = Query(None),
|
|
presenter: Optional[str] = Query(None),
|
|
min_confidence: Optional[float] = Query(None, ge=0.0, le=1.0),
|
|
limit: int = Query(50, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
store: Store = Depends(_get_store),
|
|
):
|
|
conditions = []
|
|
params: list = []
|
|
|
|
if fact_type:
|
|
conditions.append("kf.fact_type = ?")
|
|
params.append(fact_type)
|
|
if subject:
|
|
conditions.append("kf.subject LIKE ?")
|
|
params.append(f"%{subject}%")
|
|
if presenter:
|
|
conditions.append("kf.presenter LIKE ?")
|
|
params.append(f"%{presenter}%")
|
|
if min_confidence is not None:
|
|
conditions.append("kf.confidence >= ?")
|
|
params.append(min_confidence)
|
|
|
|
where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
|
|
params += [limit, offset]
|
|
|
|
rows = store.conn.execute(
|
|
f"""
|
|
SELECT kf.*, ks.video_path, ks.location
|
|
FROM knowledge_facts kf
|
|
LEFT JOIN knowledge_sources ks ON ks.id = kf.source_id
|
|
{where}
|
|
ORDER BY kf.created_at DESC
|
|
LIMIT ? OFFSET ?
|
|
""",
|
|
params,
|
|
).fetchall()
|
|
|
|
result = []
|
|
for row in rows:
|
|
d = store._row_to_dict(row)
|
|
d["payload"] = json.loads(d["payload"])
|
|
result.append(d)
|
|
return result
|
|
|
|
|
|
@router.get("/{fact_id}", response_model=KnowledgeFactOut)
|
|
async def get_knowledge_fact(fact_id: int, store: Store = Depends(_get_store)):
|
|
row = store.conn.execute(
|
|
"""
|
|
SELECT kf.*, ks.video_path, ks.location
|
|
FROM knowledge_facts kf
|
|
LEFT JOIN knowledge_sources ks ON ks.id = kf.source_id
|
|
WHERE kf.id = ?
|
|
""",
|
|
(fact_id,),
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Knowledge fact not found")
|
|
d = store._row_to_dict(row)
|
|
d["payload"] = json.loads(d["payload"])
|
|
return d
|