claude-os/app/core/pg_manager.py
Bob Roberts 53295444de Major refactor: Native Ollama setup + MCP integration
Features:
- Fixed Ollama configuration for native (localhost) instead of Docker
- Increased LLM timeout from 30s to 120s for quality answers
- Implemented KB slug system for clean MCP endpoint URLs
- Added multiple knowledge base support with type metadata
- Integrated MCP server with FastAPI (port 8051)
- React frontend with KB management UI
- PostgreSQL + pgvector for vector embeddings

Performance:
- 33-56% faster than Docker setup with native Ollama
- Direct Metal GPU acceleration on Apple Silicon
- Optimized RAG engine with caching
- ~40s response time for comprehensive answers

Documentation:
- Complete native setup guide (README_NATIVE_SETUP.md)
- Performance benchmarking and comparison
- MCP endpoint documentation
- Slug implementation details
- Decision rationale for native vs Docker

Cleanup:
- Removed old Streamlit setup files
- Consolidated setup scripts to single setup_native.sh
- Consolidated service management scripts
- Removed test artifacts and old analysis docs
- Ready for sharing with team

🚀 Production-ready for local team deployment

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 18:37:19 -05:00

542 lines
20 KiB
Python

"""
PostgreSQL + pgvector database manager for Code-Forge.
Replaces ChromaDB with a more robust, ACID-compliant solution.
"""
import os
import json
import re
from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime
import psycopg2
from psycopg2.extras import RealDictCursor, Json
from psycopg2.pool import SimpleConnectionPool
import numpy as np
from app.core.config import Config
from app.core.kb_types import KBType, KBMetadata
def generate_slug(name: str) -> str:
"""
Generate a URL-friendly slug from a KB name.
Examples:
"Pistn Agent OS" -> "pistn-agent-os"
"My Code Base!" -> "my-code-base"
"Test_KB 123" -> "test-kb-123"
"""
# Convert to lowercase
slug = name.lower()
# Replace spaces and underscores with hyphens
slug = re.sub(r'[\s_]+', '-', slug)
# Remove any characters that aren't alphanumeric or hyphens
slug = re.sub(r'[^a-z0-9-]', '', slug)
# Remove multiple consecutive hyphens
slug = re.sub(r'-+', '-', slug)
# Remove leading/trailing hyphens
slug = slug.strip('-')
return slug
class PostgresManager:
"""Manages PostgreSQL database operations with pgvector for embeddings."""
def __init__(self):
"""Initialize connection pool."""
# Build connection parameters
conn_params = {
"minconn": 1,
"maxconn": 10,
"host": os.getenv("POSTGRES_HOST", "localhost"),
"port": int(os.getenv("POSTGRES_PORT", "5432")),
"database": os.getenv("POSTGRES_DB", "codeforge"),
"user": os.getenv("POSTGRES_USER", os.getenv("USER", "postgres")),
}
# Only add password if it's set (for local PostgreSQL with trust auth)
password = os.getenv("POSTGRES_PASSWORD", "")
if password:
conn_params["password"] = password
self.pool = SimpleConnectionPool(**conn_params)
self._init_schema()
def _init_schema(self):
"""Initialize database schema from schema.sql."""
# Schema is managed externally - just verify connection
# The schema.sql should be run manually on the local PostgreSQL
pass
def get_connection(self):
"""Get a connection from the pool."""
return self.pool.getconn()
def return_connection(self, conn):
"""Return a connection to the pool."""
self.pool.putconn(conn)
# Knowledge Base Operations
def create_collection(
self,
name: str,
kb_type: KBType = KBType.GENERIC,
description: str = "",
tags: List[str] = None,
embed_dim: int = 768
) -> Dict[str, Any]:
"""Create a new knowledge base with PGVectorStore table."""
import json
conn = self.get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Generate URL-friendly slug
slug = generate_slug(name)
# Sanitize name for table name
table_name = f"data_{name.lower().replace(' ', '_').replace('-', '_')}"
# Prepare metadata (tags go in metadata JSON, not separate column)
metadata = {}
if tags:
metadata["tags"] = tags
# Insert KB metadata (no tags column in NEW schema)
cur.execute(
"""
INSERT INTO knowledge_bases (name, slug, kb_type, description, metadata, table_name, embed_dim)
VALUES (%s, %s, %s, %s, %s, %s, %s)
RETURNING id, name, slug, kb_type, description, created_at, metadata, table_name, embed_dim
""",
(name, slug, kb_type.value, description, json.dumps(metadata), table_name, embed_dim)
)
result = cur.fetchone()
# Create the PGVectorStore table using the helper function
cur.execute("SELECT create_kb_table(%s, %s, %s)", (name, table_name, embed_dim))
conn.commit()
return dict(result)
except psycopg2.IntegrityError as e:
conn.rollback()
if 'slug' in str(e):
raise ValueError(f"A knowledge base with a similar name already exists (slug conflict: '{slug}')")
raise ValueError(f"Knowledge base '{name}' already exists")
finally:
self.return_connection(conn)
def delete_collection(self, name: str) -> bool:
"""Delete a knowledge base and its PGVectorStore table."""
conn = self.get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Get table name before deleting
cur.execute("SELECT table_name FROM knowledge_bases WHERE name = %s", (name,))
result = cur.fetchone()
if not result:
return False
table_name = result['table_name']
# Drop the PGVectorStore table if it exists
if table_name:
cur.execute("SELECT drop_kb_table(%s)", (table_name,))
# Delete KB metadata
cur.execute("DELETE FROM knowledge_bases WHERE name = %s", (name,))
deleted = cur.rowcount > 0
conn.commit()
return deleted
finally:
self.return_connection(conn)
def list_collections(self) -> List[Dict[str, Any]]:
"""List all knowledge bases with metadata."""
conn = self.get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"""
SELECT name, slug, kb_type, description, created_at, metadata
FROM knowledge_bases
ORDER BY created_at DESC
"""
)
results = cur.fetchall()
return [
{
"name": row["name"],
"slug": row.get("slug") or generate_slug(row["name"]), # Fallback for old KBs
"metadata": {
"kb_type": row["kb_type"],
"description": row["description"] or "",
"created_at": row["created_at"].isoformat() if row["created_at"] else "",
**row["metadata"]
}
}
for row in results
]
finally:
self.return_connection(conn)
def get_collection_metadata(self, name: str) -> Dict[str, Any]:
"""Get metadata for a specific knowledge base."""
conn = self.get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"""
SELECT kb_type, description, created_at, tags, metadata
FROM knowledge_bases
WHERE name = %s
""",
(name,)
)
row = cur.fetchone()
if not row:
raise ValueError(f"Knowledge base '{name}' not found")
return {
"kb_type": row["kb_type"],
"description": row["description"] or "",
"created_at": row["created_at"].isoformat() if row["created_at"] else "",
"tags": row["tags"] or "",
**row["metadata"]
}
finally:
self.return_connection(conn)
def list_collections_by_type(self, kb_type: KBType) -> List[Dict[str, Any]]:
"""List knowledge bases filtered by type."""
conn = self.get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"""
SELECT name, kb_type, description, created_at, metadata
FROM knowledge_bases
WHERE kb_type = %s
ORDER BY created_at DESC
""",
(kb_type.value,)
)
results = cur.fetchall()
return [
{
"name": row["name"],
"metadata": {
"kb_type": row["kb_type"],
"description": row["description"] or "",
"created_at": row["created_at"].isoformat() if row["created_at"] else "",
**row["metadata"]
}
}
for row in results
]
finally:
self.return_connection(conn)
# Document Operations
def add_documents(
self,
kb_name: str,
documents: List[str],
embeddings: List[List[float]],
metadatas: List[Dict[str, Any]],
ids: List[str]
) -> None:
"""Add documents to a knowledge base using PGVectorStore schema."""
from llama_index.core.schema import TextNode
from llama_index.vector_stores.postgres import PGVectorStore
conn = self.get_connection()
try:
# Get KB metadata
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"SELECT id, table_name, embed_dim FROM knowledge_bases WHERE name = %s",
(kb_name,)
)
result = cur.fetchone()
if not result:
raise ValueError(f"Knowledge base '{kb_name}' not found")
kb_id = result['id']
table_name = result['table_name']
embed_dim = result['embed_dim']
if not table_name:
raise ValueError(f"Knowledge base '{kb_name}' has not been migrated to PGVectorStore schema")
finally:
self.return_connection(conn)
# Create PGVectorStore instance
pg_user = os.getenv("POSTGRES_USER", os.getenv("USER", "postgres"))
pg_password = os.getenv("POSTGRES_PASSWORD", "")
pg_host = os.getenv("POSTGRES_HOST", "localhost")
pg_port = os.getenv("POSTGRES_PORT", "5432")
pg_db = os.getenv("POSTGRES_DB", "codeforge")
# PGVectorStore automatically adds 'data_' prefix to table names
# Our schema already has 'data_' prefix, so we need to strip it
pgvector_table_name = table_name.removeprefix('data_')
# Use individual parameters to avoid async connection string issues
vector_store = PGVectorStore.from_params(
host=pg_host,
port=pg_port,
database=pg_db,
user=pg_user,
password=pg_password,
table_name=pgvector_table_name, # PGVectorStore will add 'data_' prefix
embed_dim=embed_dim,
perform_setup=False # Don't recreate tables
)
# Create TextNode objects for llama_index
nodes = []
for doc, emb, meta, doc_id in zip(documents, embeddings, metadatas, ids):
# Add kb_id to metadata
meta_with_kb = {**meta, "kb_id": str(kb_id)}
node = TextNode(
text=doc,
id_=doc_id,
embedding=emb,
metadata=meta_with_kb
)
nodes.append(node)
# Add nodes to vector store
vector_store.add(nodes)
def get_documents_by_metadata(
self,
kb_name: str,
where: Dict[str, Any],
limit: Optional[int] = None
) -> List[Dict[str, Any]]:
"""Get documents filtered by metadata from NEW PGVectorStore schema."""
conn = self.get_connection()
try:
# Get table name for this KB
with conn.cursor() as cur:
cur.execute("SELECT table_name FROM knowledge_bases WHERE name = %s", (kb_name,))
result = cur.fetchone()
if not result or not result[0]:
return []
table_name = result[0]
# Build WHERE clause for JSONB metadata_ column
where_conditions = []
params = []
for key, value in where.items():
where_conditions.append(f"metadata_->>%s = %s")
params.extend([key, str(value)])
where_clause = " AND ".join(where_conditions) if where_conditions else "TRUE"
# Query NEW schema: data_{kb_name} table with columns: id, text, metadata_, node_id, embedding
query = f"""
SELECT node_id, text, metadata_
FROM {table_name}
WHERE {where_clause}
"""
if limit:
query += f" LIMIT {limit}"
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(query, params)
results = cur.fetchall()
# Return with 'metadata' key (not 'metadata_') for compatibility
return [
{
"node_id": row["node_id"],
"text": row["text"],
"metadata": row["metadata_"]
}
for row in results
]
finally:
self.return_connection(conn)
def query_documents(
self,
kb_name: str,
query_embedding: List[float],
n_results: int = 10,
where: Optional[Dict[str, Any]] = None
) -> Dict[str, List]:
"""Query documents using vector similarity search."""
conn = self.get_connection()
try:
# Get KB ID
with conn.cursor() as cur:
cur.execute("SELECT id FROM knowledge_bases WHERE name = %s", (kb_name,))
result = cur.fetchone()
if not result:
raise ValueError(f"Knowledge base '{kb_name}' not found")
kb_id = result[0]
# Search documents
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"""
SELECT * FROM search_documents(%s, %s, %s, %s)
""",
(kb_id, query_embedding, n_results, Json(where) if where else None)
)
results = cur.fetchall()
return {
"ids": [[row["doc_id"] for row in results]],
"documents": [[row["content"] for row in results]],
"metadatas": [[row["metadata"] for row in results]],
"distances": [[1 - row["similarity"] for row in results]]
}
finally:
self.return_connection(conn)
def collection_exists(self, kb_name: str) -> bool:
"""Check if a knowledge base exists by name."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
cur.execute("SELECT EXISTS(SELECT 1 FROM knowledge_bases WHERE name = %s)", (kb_name,))
return cur.fetchone()[0]
finally:
self.return_connection(conn)
def get_kb_by_slug(self, slug: str) -> Optional[str]:
"""Get KB name by slug. Returns None if not found."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
cur.execute("SELECT name FROM knowledge_bases WHERE slug = %s", (slug,))
result = cur.fetchone()
return result[0] if result else None
finally:
self.return_connection(conn)
def slug_exists(self, slug: str) -> bool:
"""Check if a slug exists."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
cur.execute("SELECT EXISTS(SELECT 1 FROM knowledge_bases WHERE slug = %s)", (slug,))
return cur.fetchone()[0]
finally:
self.return_connection(conn)
def get_collection_count(self, kb_name: str) -> int:
"""Get document count for a knowledge base."""
conn = self.get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Get table name
cur.execute("SELECT table_name FROM knowledge_bases WHERE name = %s", (kb_name,))
result = cur.fetchone()
if not result or not result['table_name']:
return 0
table_name = result['table_name']
# Count documents in PGVectorStore table
cur.execute(f"SELECT COUNT(*) as count FROM {table_name}")
count_result = cur.fetchone()
return count_result['count'] if count_result else 0
finally:
self.return_connection(conn)
def close(self):
"""Close all connections in the pool."""
if self.pool:
self.pool.closeall()
def query_similar(
self,
kb_id: int,
query_embedding: List[float],
top_k: int = 5
) -> List[Dict[str, Any]]:
"""
Query for similar documents using vector similarity.
Works with NEW PGVectorStore schema (data_{kb_name} tables).
Args:
kb_id: Knowledge base ID
query_embedding: Query embedding vector
top_k: Number of results to return
Returns:
List of similar documents with text, metadata, and similarity score
"""
import logging
logger = logging.getLogger(__name__)
conn = self.get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# Get table name for this KB
cur.execute("SELECT table_name FROM knowledge_bases WHERE id = %s", (kb_id,))
result = cur.fetchone()
if not result or not result['table_name']:
logger.warning(f"No table found for kb_id={kb_id}")
return []
table_name = result['table_name']
# Convert embedding list to PostgreSQL vector format string
# Format: '[0.123, 0.456, ...]'
embedding_str = "[" + ",".join(map(str, query_embedding)) + "]"
logger.info(f"Query embedding dimension: {len(query_embedding)}")
logger.info(f"Querying kb_id={kb_id}, table={table_name}, top_k={top_k}")
# Query for similar documents using cosine similarity
# NEW schema uses 'text' and 'metadata_' columns
query = f"""
SELECT
text,
metadata_ as metadata,
node_id,
1 - (embedding <=> %s::vector) as similarity
FROM {table_name}
ORDER BY embedding <=> %s::vector
LIMIT %s
"""
logger.info(f"Executing query with params: top_k={top_k}")
# Pass the embedding as a string with ::vector cast
cur.execute(query, (embedding_str, embedding_str, top_k))
results = cur.fetchall()
logger.info(f"PostgreSQL returned {len(results)} results")
if results:
logger.info(f"First result similarity: {results[0].get('similarity')}")
return [dict(row) for row in results]
except Exception as e:
logger.error(f"Error in query_similar: {e}")
raise
finally:
self.return_connection(conn)
# Singleton instance
_pg_manager = None
def get_pg_manager() -> PostgresManager:
"""Get or create the PostgresManager singleton."""
global _pg_manager
if _pg_manager is None:
_pg_manager = PostgresManager()
return _pg_manager