Self-Vision-Space / pinecone_client.py
banao-tech's picture
Upload 2 files
76fd3f6 verified
Raw
History Blame Contribute Delete
9.23 kB
"""
Pinecone client for Self Vision POC.
Uses the existing `live-assistant-index-v2` index.
Stores conversations as metadata on dummy zero-vectors (no embeddings, no semantic search).
All operations are wrapped in try/except — failures are logged, never crash the app.
"""
import os
import time
import logging
from pinecone import Pinecone
logger = logging.getLogger("self_vision.pinecone")
INDEX_NAME = "live-assistant-index-v2"
# Keys used across POCs
KEY_ONBOARDING = "onboarding_conversation"
KEY_SELF_VISION = "self_vision_conversation"
KEY_ROADMAP = "roadmap_output"
# Namespace for roadmap_id → user_id reverse index
NAMESPACE_ROADMAP_INDEX = "_roadmap_index"
class PineconeClient:
"""Thin wrapper around Pinecone for key-value conversation storage."""
def __init__(self):
api_key = os.getenv("PINECONE_API_KEY", "")
if not api_key:
logger.warning("PINECONE_API_KEY not set — Pinecone operations will be no-ops")
self._index = None
self._dimension = 3072
return
try:
pc = Pinecone(api_key=api_key)
self._index = pc.Index(INDEX_NAME)
# Discover index dimension dynamically
stats = self._index.describe_index_stats()
self._dimension = stats.get("dimension", 1536)
logger.info(
"Pinecone connected: index=%s dimension=%d",
INDEX_NAME,
self._dimension,
)
except Exception as exc:
logger.error("Pinecone init failed: %s", exc)
self._index = None
self._dimension = 3072
# ------------------------------------------------------------------
# READ
# ------------------------------------------------------------------
def fetch_conversation(self, user_id: str, key: str) -> dict:
"""
Fetch a conversation blob from namespace=user_id, id=key.
Returns:
dict with:
- 'text': the conversation text (empty string if missing)
- 'status': 'success' | 'empty' | 'error'
- 'elapsed_ms': fetch duration in milliseconds
"""
start = time.time()
if self._index is None:
elapsed = (time.time() - start) * 1000
logger.warning("FETCH_ERROR: Pinecone not initialised")
return {"text": "", "status": "error", "elapsed_ms": elapsed}
try:
vector_id = f"{user_id}_{key}"
result = self._index.fetch(ids=[vector_id], namespace=user_id)
elapsed = (time.time() - start) * 1000
vectors = result.get("vectors", {})
if vector_id in vectors:
metadata = vectors[vector_id].get("metadata", {})
text = metadata.get("text", "")
if text:
logger.info(
"Fetched key=%s from namespace=%s (%d chars, %.0fms)",
vector_id, user_id, len(text), elapsed,
)
return {"text": text, "status": "success", "elapsed_ms": elapsed}
logger.info(
"No record for key=%s in namespace=%s (%.0fms)",
vector_id, user_id, elapsed,
)
return {"text": "", "status": "empty", "elapsed_ms": elapsed}
except Exception as exc:
elapsed = (time.time() - start) * 1000
logger.error("FETCH_ERROR: %s (%.0fms)", exc, elapsed)
return {"text": "", "status": "error", "elapsed_ms": elapsed}
# ------------------------------------------------------------------
# WRITE
# ------------------------------------------------------------------
def upsert_conversation(self, user_id: str, key: str, text: str) -> dict:
"""
Upsert a single conversation blob.
namespace = user_id
id = key
metadata = {"text": <full raw text>}
vector = dummy zero-vector (no embeddings used)
Returns:
dict with:
- 'status': 'success' | 'error'
- 'elapsed_ms': upsert duration in milliseconds
"""
start = time.time()
if self._index is None:
elapsed = (time.time() - start) * 1000
logger.warning("UPSERT_ERROR: Pinecone not initialised")
return {"status": "error", "elapsed_ms": elapsed}
try:
dummy_vector = [0.1] * self._dimension
vector_id = f"{user_id}_{key}"
self._index.upsert(
vectors=[
{
"id": vector_id,
"values": dummy_vector,
"metadata": {"text": text},
}
],
namespace=user_id,
)
elapsed = (time.time() - start) * 1000
logger.info(
"Upserted key=%s in namespace=%s (%d chars, %.0fms)",
vector_id, user_id, len(text), elapsed,
)
# Auto-store roadmap_id → user_id mapping when upserting a roadmap
if key == KEY_ROADMAP and text:
try:
import json
roadmap_data = json.loads(text)
roadmap_id = roadmap_data.get("roadmap_id") or roadmap_data.get("ai_roadmap_id", "")
if roadmap_id:
self.store_roadmap_mapping(roadmap_id, user_id)
except Exception as map_exc:
logger.warning("Could not store roadmap mapping: %s", map_exc)
return {"status": "success", "elapsed_ms": elapsed}
except Exception as exc:
elapsed = (time.time() - start) * 1000
logger.error("UPSERT_ERROR: %s (%.0fms)", exc, elapsed)
return {"status": "error", "elapsed_ms": elapsed}
# ------------------------------------------------------------------
# ROADMAP INDEX — reverse lookup: roadmap_id → user_id
# ------------------------------------------------------------------
def store_roadmap_mapping(self, roadmap_id: str, user_id: str) -> dict:
"""
Store a roadmap_id → user_id mapping in the reverse index.
namespace = _roadmap_index
id = roadmap_id
metadata = {"user_id": user_id}
"""
start = time.time()
if self._index is None:
elapsed = (time.time() - start) * 1000
return {"status": "error", "elapsed_ms": elapsed}
try:
dummy_vector = [0.1] * self._dimension
self._index.upsert(
vectors=[
{
"id": roadmap_id,
"values": dummy_vector,
"metadata": {"user_id": user_id},
}
],
namespace=NAMESPACE_ROADMAP_INDEX,
)
elapsed = (time.time() - start) * 1000
logger.info(
"Stored roadmap mapping: %s → %s (%.0fms)",
roadmap_id, user_id, elapsed,
)
return {"status": "success", "elapsed_ms": elapsed}
except Exception as exc:
elapsed = (time.time() - start) * 1000
logger.error("ROADMAP_INDEX_ERROR: %s (%.0fms)", exc, elapsed)
return {"status": "error", "elapsed_ms": elapsed}
def resolve_user_from_roadmap(self, roadmap_id: str) -> dict:
"""
Look up user_id from a roadmap_id via the reverse index.
Returns:
dict with:
- 'user_id': the resolved user_id (empty string if not found)
- 'status': 'success' | 'empty' | 'error'
- 'elapsed_ms': lookup duration in milliseconds
"""
start = time.time()
if self._index is None:
elapsed = (time.time() - start) * 1000
return {"user_id": "", "status": "error", "elapsed_ms": elapsed}
try:
result = self._index.fetch(
ids=[roadmap_id],
namespace=NAMESPACE_ROADMAP_INDEX,
)
elapsed = (time.time() - start) * 1000
vectors = result.get("vectors", {})
if roadmap_id in vectors:
metadata = vectors[roadmap_id].get("metadata", {})
user_id = metadata.get("user_id", "")
if user_id:
logger.info(
"Resolved roadmap %s → user %s (%.0fms)",
roadmap_id, user_id, elapsed,
)
return {"user_id": user_id, "status": "success", "elapsed_ms": elapsed}
logger.info(
"No mapping for roadmap_id=%s (%.0fms)",
roadmap_id, elapsed,
)
return {"user_id": "", "status": "empty", "elapsed_ms": elapsed}
except Exception as exc:
elapsed = (time.time() - start) * 1000
logger.error("ROADMAP_RESOLVE_ERROR: %s (%.0fms)", exc, elapsed)
return {"user_id": "", "status": "error", "elapsed_ms": elapsed}