openweb / vrag /remote.py
Leon4gr45's picture
Upload folder using huggingface_hub
33962ff verified
Raw History Blame Contribute Delete
4.1 kB
"""CPU RAG facade: fail explicitly on incompatible index/provider combinations."""
import asyncio
import base64
import hashlib
import json
import os
import time
from datetime import datetime, timezone
import numpy as np
from .config import DATA, INDEX_DIR, VIDEOS_DIR
from .index import get_index
from .providers import ProviderFailure
from .routing import route
from .settings import store
def space_id(provider):
# Endpoint reference plus model and dimension identify a distinct embedding space.
return hashlib.sha256(f'{os.environ.get(provider.base_url_env, "")}:{provider.model}'.encode()).hexdigest()[:16]
async def query(text, config=None):
if config is None:
_, config = store.snapshot()
rule = config.features.get('rag.text_embeddings')
if not rule or not rule.enabled or rule.strategy != 'single':
raise ProviderFailure('embedding_provider_unavailable')
p = config.providers[rule.providers[0]]
manifest_path = get_index().path / 'manifest.json'
manifest = json.loads(manifest_path.read_text()) if manifest_path.exists() else {}
if manifest.get('embedding_space') != space_id(p):
raise ProviderFailure('index_incompatible_reembed_required')
out, statuses = await route(config, 'rag.text_embeddings', {'text': text})
if out.embedding is None:
raise ProviderFailure(statuses[-1]['status'] if statuses else 'embedding_provider_unavailable')
if len(out.embedding) != manifest.get('dimension'):
raise ProviderFailure('index_dimension_mismatch')
q = np.asarray(out.embedding, dtype=np.float32)
return q / np.linalg.norm(q)
def encode_query(text):
return asyncio.run(query(text))
def encode_image_query(path):
raise ProviderFailure('image_embeddings_unsupported')
def build_messages(question, hits, use_video, use_audio):
if use_audio:
raise ProviderFailure('rag_audio_unsupported_use_remote_transcription')
text = 'Answer only from these clips and cite [n]. Say when evidence is insufficient.\nQuestion: ' + question
images = []
for n, h in enumerate(hits, 1):
text += f'\n[{n}] {h["title"]} {h["start"]}-{h["end"]} seconds: {h["text"]}'
if use_video:
# Existing ingestion produces a bounded midpoint frame per segment. Remote models receive bytes, never file://.
frame = VIDEOS_DIR / h['video_id'] / 'segments' / f'{h["idx"]:03d}.jpg'
if frame.exists() and frame.stat().st_size <= 1024 * 1024:
images.append('data:image/jpeg;base64,' + base64.b64encode(frame.read_bytes()).decode())
return {'text': text[:64000], 'images': images[:8]}
async def _answer(question, view, dim, k, use_video, use_audio):
start = time.monotonic()
_, config = store.snapshot()
hits = get_index().search(await query(question, config), view=view, dim=dim, k=min(k, 8))
retrieve = time.monotonic() - start
out, statuses = await route(config, 'rag.reasoning', build_messages(question, hits, use_video, use_audio))
if not out.text:
raise ProviderFailure('reasoning_provider_unavailable')
result = {'question': question, 'answer': out.text, 'clips': hits,
'settings': {'view': view, 'dim': dim, 'k': k, 'use_video': use_video, 'use_audio': use_audio},
'usage': {'prompt_tokens': 0, 'completion_tokens': 0}, 'usage_available': False,
'provider_results': statuses, 'timing': {'retrieve_s': retrieve, 'generate_s': time.monotonic()-start-retrieve},
'created_at': datetime.now(timezone.utc).isoformat()}
if os.environ.get('VRAG_SAVE_ANSWERS', 'true').lower() == 'true':
from .backup.github import state_lock
async with state_lock:
history = DATA / 'answers' / 'history.jsonl'
history.parent.mkdir(parents=True, exist_ok=True)
with history.open('a') as f:
f.write(json.dumps(result) + '\n')
return result
def answer(question, view='text', dim=768, k=4, use_video=True, use_audio=False):
return asyncio.run(_answer(question, view, dim, k, use_video, use_audio))