Major updates to use react and agent os profiles

This commit is contained in:
Bob Roberts 2025-10-23 19:23:05 -05:00
parent 8e5a46aa59
commit a6a9c77f51
48 changed files with 12242 additions and 1237 deletions

View file

@ -17,6 +17,10 @@ CHUNK_OVERLAP=200
TOP_K_RETRIEVAL=5
RERANK_TOP_N=3
# KB Type Configuration
# Default type for new knowledge bases: generic, code, documentation, agent-os
DEFAULT_KB_TYPE=generic
# Storage Configuration
UPLOAD_DIR=/workspace/data/uploads

14
.gitignore vendored
View file

@ -26,6 +26,18 @@ env/
ENV/
.venv
# Node.js / React / Frontend
node_modules/
frontend/dist/
frontend/.vite/
*.tsbuildinfo
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.npm
.yarn
# IDE
.vscode/
.idea/
@ -40,6 +52,8 @@ ENV/
# Data Directories
data/chroma/
data/uploads/
data/ollama/
data/postgres/
*.db
*.sqlite

View file

@ -15,3 +15,11 @@ enableCORS = false
[browser]
gatherUsageStats = false
[ui]
# Sidebar configuration
hideTopBar = false
hideSidebarNav = false
[client]
toolbarMode = "minimal"

472
README.md
View file

@ -1,32 +1,106 @@
# 🔥 Code-Forge
# Code-Forge
**Localized Multi-Knowledge-Base RAG System with MCP Integration**
<div align="center">
A self-hosted, privacy-first RAG (Retrieval-Augmented Generation) application designed for development teams. Features a badass Archon-inspired UI and exposes knowledge bases via Model Context Protocol (MCP) for AI agent integration.
```
╔═══════════════════════════════════════════════════════════════╗
║ ║
║ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
║ ██║ ██║ ██║██║ ██║█████╗ █████╗ ██║ ██║██████╔╝██║ ███╗█████╗
║ ██║ ██║ ██║██║ ██║██╔══╝ ██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝
║ ╚██████╗╚██████╔╝██████╔╝███████╗ ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗
║ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝
║ ║
║ Localized Multi-Knowledge-Base RAG System ║
║ with MCP Integration ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
```
**Production-grade RAG system with PostgreSQL + pgvector, Ollama LLMs, and Model Context Protocol**
[![License: MIT](https://img.shields.io/badge/License-MIT-purple.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![PostgreSQL 17](https://img.shields.io/badge/PostgreSQL-17-green.svg)](https://www.postgresql.org/)
[![Ollama](https://img.shields.io/badge/Ollama-Latest-pink.svg)](https://ollama.ai/)
</div>
---
## 🚀 What is Code-Forge?
**Code-Forge** is a **localized, production-grade RAG (Retrieval-Augmented Generation) system** that runs entirely on your machine. It combines:
- **🗄️ PostgreSQL + pgvector** - ACID-compliant vector database with 768-dimensional embeddings
- **🤖 Ollama** - Local LLMs (llama3.2, llama3.1) and embeddings (nomic-embed-text)
- **🔌 MCP (Model Context Protocol)** - HTTP API for AI agent integration
- **⚡ React + FastAPI UI** - Modern, beautiful interface with electric teal design
- **📚 Multi-KB Architecture** - Isolated knowledge bases per project
- **🧠 Advanced RAG** - Vector search, hybrid search, reranking, agentic RAG
### Why Code-Forge?
✅ **100% Local** - Your data never leaves your machine
✅ **Production-Ready** - PostgreSQL ensures data persistence and ACID compliance
✅ **Multi-Knowledge-Base** - Separate KBs for different projects/contexts
✅ **Agent OS Support** - First-class support for spec-driven development
✅ **Advanced RAG** - Multiple retrieval strategies for optimal results
✅ **MCP Integration** - Expose knowledge bases to AI agents via HTTP
---
## ✨ Features
### 🎯 Core Capabilities
- **Multiple Knowledge Bases**: Create isolated KBs for different projects
- **Advanced RAG Strategies**:
- 🔍 Vector Search (semantic similarity)
- 🔀 Hybrid Search (vector + BM25 keyword)
- 🎯 Reranking (cross-encoder scoring)
- 🤖 Agentic RAG (multi-step reasoning)
- **MCP Integration**: Expose KBs to AI agents like Claude Desktop
- **Dual Interface**: Streamlit UI for humans + MCP server for agents
### Core Features
### 🎨 UI/UX
- **Archon-Inspired Design**: Dark theme with neon accents
- **Color Scheme**:
- Primary: `hsl(271, 91%, 65%)` (Purple)
- Accent: `hsl(330, 90%, 65%)` (Pink)
- Success: `hsl(160, 84%, 39%)` (Green)
- Background: Pure black `#0E0E0E`
- **Real-time Health Monitoring**: Ollama & ChromaDB status
- **Document Management**: Upload files or import entire directories
- **🗄️ PostgreSQL + pgvector Vector Store**
- ACID-compliant transactions
- 768-dimensional vector embeddings
- IVFFlat indexing for fast similarity search
- JSONB metadata for flexible filtering
- **🤖 Local LLM Integration**
- Ollama-powered LLMs (llama3.2:3b, llama3.1)
- nomic-embed-text embeddings (768 dimensions)
- No API keys or cloud dependencies
- **📚 Multi-Knowledge-Base Architecture**
- Isolated KBs per project
- 4 KB types: GENERIC, CODE, DOCUMENTATION, AGENT_OS
- Type-specific ingestion and retrieval
- **🧠 Advanced RAG Strategies**
- **Vector Search** - Semantic similarity via pgvector
- **Hybrid Search** - BM25 + vector fusion
- **Reranking** - Cross-encoder for top results
- **Agentic RAG** - Sub-question decomposition
- **🔌 MCP (Model Context Protocol) Server**
- 12 HTTP endpoints for AI agent integration
- Create, query, and manage knowledge bases
- Agent OS-specific tools (standards, specs, workflows)
- **⚡ Modern React + FastAPI UI**
- Electric teal design system
- Drag & drop document upload
- Real-time chat interface with markdown rendering
- Interactive KB management dashboard
- Built-in help documentation
- **🎨 Legacy Streamlit UI** (Still available)
- Alternative interface on port 8501
- Full feature parity with React UI
### Knowledge Base Types
| Type | Icon | Description | Use Case |
|------|------|-------------|----------|
| **GENERIC** | 📚 | General-purpose knowledge | Documentation, notes, research |
| **CODE** | 💻 | Source code repositories | Code search, API reference |
| **DOCUMENTATION** | 📖 | Technical documentation | User guides, API docs |
| **AGENT_OS** | 🤖 | Spec-driven development | Standards, workflows, specs |
### 📁 Supported File Types
- **Documents**: `.md`, `.txt`, `.pdf`
@ -38,79 +112,125 @@ A self-hosted, privacy-first RAG (Retrieval-Augmented Generation) application de
## 🏗️ Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Code-Forge │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Streamlit │ │ MCP Server │ │
│ │ UI (8501) │ │ (8051) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ RAG Engine │ │
│ │ (LlamaIndex) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────────────┴─────────────┐ │
│ │ │ │
│ ┌────▼─────┐ ┌──────▼──────┐ │
│ │ ChromaDB │ │ Ollama │ │
│ │ (8000) │ │ (11434) │ │
│ └──────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Code-Forge System │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ React UI │ │ Streamlit │ │ MCP Server │ │
│ │ (Vite) │ │ UI │ │ (HTTP) │ │
│ │ Port 5173 │ │ Port 8501 │ │ Port 8051 │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┴──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ FastAPI Backend │ │
│ │ (REST + MCP) │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ RAG Engine │ │
│ │ (llama-index) │ │
│ │ │ │
│ │ • Vector Search │ │
│ │ • Hybrid Search │ │
│ │ • Reranking │ │
│ │ • Agentic RAG │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ PostgresManager │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────┐ │
│ │ PostgreSQL + pgvector │ │
│ │ (Local Database) │ │
│ │ │ │
│ │ Tables: │ │
│ │ • knowledge_bases │ │
│ │ • documents (with embeddings) │ │
│ │ • agent_os_content │ │
│ └───────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Ollama │ │ Ollama LLM │ │
│ │ Container │──────│ Service │ │
│ │ Port 11434 │ │ │ │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Data Flow
1. **Ingestion**: Documents → Chunking → Embedding → PostgreSQL
2. **Query**: User query → Embedding → Vector search → Reranking → LLM → Response
3. **MCP**: AI Agent → HTTP request → RAG Engine → Response
### Services
1. **Ollama** (port 11434): Local LLM and embeddings
2. **ChromaDB** (port 8000): Vector database
3. **App Container**:
- Streamlit UI (port 8501)
- MCP Server (port 8051)
1. **Frontend Container** (port 5173): React + Vite UI with hot reload
2. **App Container** (ports 8501, 8051):
- FastAPI backend with MCP server
- Streamlit UI (legacy)
3. **Ollama Container** (port 11434): Local LLM and embeddings
4. **PostgreSQL** (local, not Docker): Vector database with pgvector extension
---
## 🚀 Quick Start
## ⚡ Quick Start
### Prerequisites
- **Docker** and **Docker Compose** installed
- **Ollama** installed locally (or use Docker Ollama)
- At least 8GB RAM recommended
### One-Command Setup
### 1. Clone and Navigate
```bash
git clone <your-repo-url>
cd code-forge
./setup.sh
```
### 2. Pull Required Ollama Models
```bash
# Pull LLM model
ollama pull llama3.1
This automated script will:
- ✅ Check for Docker, PostgreSQL, Ollama
- ✅ Install pgvector extension
- ✅ Create `codeforge` database
- ✅ Download required Ollama models (~7GB)
- ✅ Build and start Docker containers
- ✅ Verify installation
# Pull embedding model
ollama pull nomic-embed-text
### Start the Frontend
```bash
cd frontend
npm install
npm run dev
```
### 3. One-Command Deployment
```bash
docker compose up --build
```
### Access the Application
This will:
- Start Ollama service
- Start ChromaDB service
- Build and start the Code-Forge application
- Wait for all services to become healthy
- **React UI**: http://localhost:5173 **(Recommended)**
- **Streamlit UI**: http://localhost:8501 (Legacy)
- **MCP Server**: http://localhost:8051
- **Ollama API**: http://localhost:11434
### 4. Access the Application
---
**Streamlit UI**: http://localhost:8501
## 📦 Prerequisites
**MCP Server**: http://localhost:8051/mcp
### Required
- **macOS** (tested on macOS 14+)
- **Docker Desktop** - [Download](https://www.docker.com/products/docker-desktop)
- **PostgreSQL 17+** - [Download Postgres.app](https://postgresapp.com/) or `brew install postgresql@17`
- **Ollama** - [Download](https://ollama.ai/download) or `brew install ollama`
### Disk Space
- **PostgreSQL**: ~500MB
- **Ollama Models**: ~7GB
- llama3.2:3b (2.0 GB)
- llama3.1:latest (4.9 GB)
- nomic-embed-text (274 MB)
- **Docker Images**: ~2GB
**Total**: ~10GB
---
@ -232,50 +352,39 @@ List all documents in a knowledge base.
### Environment Variables
Create a `.env` file to customize settings:
Create `.env` file:
```bash
# Ollama Configuration
# Ollama
OLLAMA_HOST=http://ollama:11434
OLLAMA_MODEL=llama3.1
OLLAMA_MODEL=llama3.2:3b
OLLAMA_EMBED_MODEL=nomic-embed-text
# ChromaDB Configuration
CHROMA_HOST=chromadb
CHROMA_PORT=8000
# PostgreSQL
POSTGRES_HOST=host.docker.internal
POSTGRES_PORT=5432
POSTGRES_DB=codeforge
POSTGRES_USER=your_username
# MCP Server Configuration
MCP_SERVER_HOST=0.0.0.0
MCP_SERVER_PORT=8051
# RAG Configuration
CHUNK_SIZE=1024
CHUNK_OVERLAP=200
# RAG
TOP_K_RETRIEVAL=5
RERANK_TOP_N=3
# Storage
UPLOAD_DIR=/workspace/data/uploads
CHUNK_SIZE=512
CHUNK_OVERLAP=50
```
### Changing Models
### RAG Strategy Tuning
To use different Ollama models:
Edit `app/core/config.py`:
1. Pull the model:
```bash
ollama pull mistral
```
```python
# Retrieval settings
TOP_K_RETRIEVAL = 5
RERANK_TOP_N = 3
2. Update `.env`:
```bash
OLLAMA_MODEL=mistral
```
3. Restart the application:
```bash
docker compose restart app
```
# Chunking settings
CHUNK_SIZE = 512
CHUNK_OVERLAP = 50
```
---
@ -287,25 +396,28 @@ To use different Ollama models:
code-forge/
├── app/
│ ├── core/
│ │ ├── config.py # Configuration management
│ │ ├── health.py # Service health checks
│ │ ├── chroma_manager.py # ChromaDB operations
│ │ ├── ingestion.py # Document processing
│ │ ├── kb_metadata.py # KB metadata management
│ │ └── rag_engine.py # RAG implementation
│ └── main.py # Streamlit application
│ │ ├── config.py # Configuration management
│ │ ├── pg_manager.py # PostgreSQL operations
│ │ ├── ingestion.py # Document processing
│ │ ├── agent_os_ingestion.py # Agent OS profile ingestion
│ │ ├── kb_metadata.py # KB metadata management
│ │ ├── kb_types.py # KB type definitions
│ │ └── rag_engine.py # RAG implementation
│ ├── db/
│ │ └── schema.sql # PostgreSQL schema
│ └── main.py # Streamlit application
├── mcp_server/
│ └── server.py # MCP server (FastMCP + FastAPI)
│ └── server.py # MCP server (FastMCP)
├── .streamlit/
│ └── config.toml # Streamlit theme
│ └── config.toml # Streamlit theme (Archon colors)
├── data/
│ ├── chroma/ # ChromaDB persistent storage
│ └── uploads/ # Uploaded documents
├── docker-compose.yml # Service orchestration
├── Dockerfile # Application container
├── requirements.txt # Python dependencies
├── start.sh # Startup script
└── README.md # This file
│ └── ollama/ # Ollama models (persistent)
├── docs/ # Documentation
├── docker-compose.yml # Service orchestration
├── Dockerfile # Application container
├── requirements.txt # Python dependencies
├── setup.sh # Automated setup script
└── README.md # This file
```
### Running Locally (Without Docker)
@ -315,7 +427,7 @@ code-forge/
pip install -r requirements.txt
```
2. Start Ollama and ChromaDB separately
2. Ensure PostgreSQL and Ollama are running locally
3. Start the MCP server:
```bash
@ -331,99 +443,91 @@ code-forge/
## 🐛 Troubleshooting
### Services Not Starting
### PostgreSQL Connection Failed
**Check service health:**
```bash
docker compose ps
```
# Check if PostgreSQL is running
psql -c "SELECT version();"
**View logs:**
```bash
docker compose logs ollama
docker compose logs chromadb
docker compose logs app
# If using Postgres.app, ensure it's started
# Check: Applications → Postgres.app
```
### Ollama Models Not Found
**Pull models manually:**
```bash
docker compose exec ollama ollama pull llama3.1
docker compose exec ollama ollama pull nomic-embed-text
# List installed models
ollama list
# Re-download if needed
ollama pull nomic-embed-text
ollama pull llama3.2:3b
```
### ChromaDB Connection Issues
### Docker Container Won't Start
**Restart ChromaDB:**
```bash
docker compose restart chromadb
# Check logs
docker-compose logs -f app
# Rebuild
docker-compose down
docker-compose build --no-cache
docker-compose up -d
```
**Check ChromaDB health:**
### Data Persistence Issues
Your data is stored in:
- **PostgreSQL**: Local database (survives all Docker operations)
- **Ollama models**: `./data/ollama/` (survives all Docker operations)
To verify:
```bash
curl http://localhost:8000/api/v1/heartbeat
# Check database
psql -d codeforge -c "\dt"
# Check models
ls -lh data/ollama/
```
### MCP Connection Fails
**Verify MCP server is running:**
```bash
curl http://localhost:8051/health
```
**Check MCP logs:**
```bash
docker compose logs app | grep MCP
```
See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for more solutions.
---
## 📊 Performance Tips
## 🤝 Contributing
1. **Chunk Size**: Larger chunks (1024-2048 tokens) for technical docs, smaller (512-1024) for code
2. **Hybrid Search**: Best for queries with specific keywords or technical terms
3. **Reranking**: Adds latency but significantly improves relevance
4. **Agentic RAG**: Use for complex questions requiring multi-step reasoning (slower)
Contributions welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests
5. Submit a pull request
---
## 🔒 Privacy & Security
## 📄 License
- **100% Local**: All data stays on your machine
- **No External APIs**: Ollama runs models locally
- **No Telemetry**: ChromaDB telemetry disabled
- **Isolated KBs**: Each project has its own knowledge base
---
## 📝 License
MIT License - See LICENSE file for details
MIT License - see [LICENSE](LICENSE) for details.
---
## 🙏 Acknowledgments
- **LlamaIndex**: RAG framework
- **Ollama**: Local LLM runtime
- **ChromaDB**: Vector database
- **Streamlit**: UI framework
- **FastMCP**: MCP server library
- **Archon**: Design inspiration
- **llama-index** - RAG framework
- **Ollama** - Local LLM runtime
- **pgvector** - PostgreSQL vector extension
- **Streamlit** - UI framework
- **FastMCP** - MCP server library
---
## 🚀 Roadmap
<div align="center">
- [ ] Multi-modal support (images, diagrams)
- [ ] Graph RAG for relationship extraction
- [ ] Query caching for faster responses
- [ ] Batch document processing
- [ ] Export/import knowledge bases
- [ ] Advanced analytics dashboard
**Built with ❤️ for the local-first AI community**
---
[Documentation](docs/) • [Issues](https://github.com/yourusername/code-forge/issues) • [Discussions](https://github.com/yourusername/code-forge/discussions)
**Built with 🔥 by The Augster**
</div>

BIN
app/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -0,0 +1,279 @@
"""
Agent OS Ingestion Module
Handles ingestion of Agent OS profiles into Code-Forge knowledge bases.
Integrates with ChromaManager to store parsed Agent OS content.
"""
import logging
from typing import List, Dict, Any, Optional
from pathlib import Path
from llama_index.embeddings.ollama import OllamaEmbedding
from app.core.agent_os_parser import AgentOSParser, AgentOSDocument, AgentOSContentType
from app.core.pg_manager import PostgresManager
from app.core.config import Config
logger = logging.getLogger(__name__)
class AgentOSIngestion:
"""Handles ingestion of Agent OS profiles into knowledge bases."""
def __init__(self, db_manager: PostgresManager):
"""
Initialize Agent OS ingestion.
Args:
db_manager: PostgresManager instance for storage
"""
self.db_manager = db_manager
self.parser = AgentOSParser()
def ingest_profile(
self,
kb_name: str,
profile_path: str,
batch_size: int = 100
) -> Dict[str, Any]:
"""
Ingest an Agent OS profile into a knowledge base.
Args:
kb_name: Name of the knowledge base
profile_path: Path to the Agent OS profile directory
batch_size: Number of documents to process in each batch
Returns:
Dictionary with ingestion statistics
Raises:
ValueError: If KB doesn't exist or profile path is invalid
"""
logger.info(f"Starting Agent OS profile ingestion: {profile_path} -> {kb_name}")
# Validate KB exists
collections = self.db_manager.list_collections()
kb_names = [col["name"] for col in collections]
if kb_name not in kb_names:
raise ValueError(f"Knowledge base '{kb_name}' does not exist")
# Parse the profile directory
try:
documents = self.parser.parse_directory(profile_path)
except Exception as e:
logger.error(f"Failed to parse Agent OS profile: {e}")
raise ValueError(f"Failed to parse Agent OS profile: {e}")
if not documents:
logger.warning("No documents found in Agent OS profile")
return {
"success": False,
"message": "No documents found in profile",
"documents_processed": 0
}
# Ingest documents in batches
stats = {
"total_documents": len(documents),
"documents_by_type": {},
"success": True,
"errors": []
}
# Count documents by type
for doc in documents:
content_type = doc.content_type.value
stats["documents_by_type"][content_type] = \
stats["documents_by_type"].get(content_type, 0) + 1
# Process in batches
for i in range(0, len(documents), batch_size):
batch = documents[i:i + batch_size]
try:
self._ingest_batch(kb_name, batch)
logger.info(f"Ingested batch {i//batch_size + 1}: {len(batch)} documents")
except Exception as e:
error_msg = f"Failed to ingest batch {i//batch_size + 1}: {e}"
logger.error(error_msg)
stats["errors"].append(error_msg)
stats["success"] = False
logger.info(f"Agent OS ingestion complete: {stats}")
return stats
def _ingest_batch(self, kb_name: str, documents: List[AgentOSDocument]) -> None:
"""
Ingest a batch of documents into the knowledge base.
Args:
kb_name: Name of the knowledge base
documents: List of AgentOSDocument objects to ingest
"""
if not documents:
return
# Prepare data for ChromaDB
texts = []
metadatas = []
ids = []
for idx, doc in enumerate(documents):
# Create unique ID
doc_id = f"{doc.content_type.value}_{Path(doc.file_path).stem}_{idx}"
# Prepare metadata (ChromaDB only supports string, int, float, bool)
metadata = {
"content_type": doc.content_type.value,
"title": doc.title,
"file_path": doc.file_path,
"source": "agent_os",
}
# Add document metadata (convert complex types to strings)
for key, value in doc.metadata.items():
if isinstance(value, (str, int, float, bool)):
metadata[key] = value
elif isinstance(value, list):
# Serialize lists as comma-separated strings
metadata[key] = ",".join(str(v) for v in value)
else:
# Convert other types to string
metadata[key] = str(value)
texts.append(doc.content)
metadatas.append(metadata)
ids.append(doc_id)
# Generate embeddings
embed_model = OllamaEmbedding(
model_name=Config.OLLAMA_EMBED_MODEL,
base_url=Config.OLLAMA_HOST
)
embeddings = [embed_model.get_text_embedding(text) for text in texts]
# Add to PostgreSQL
self.db_manager.add_documents(
kb_name=kb_name,
documents=texts,
embeddings=embeddings,
metadatas=metadatas,
ids=ids
)
logger.debug(f"Added {len(documents)} documents to {kb_name}")
def get_profile_stats(self, kb_name: str) -> Dict[str, Any]:
"""
Get statistics about Agent OS content in a knowledge base.
Args:
kb_name: Name of the knowledge base
Returns:
Dictionary with statistics
"""
try:
# Get all documents with agent_os source
results = self.db_manager.get_documents_by_metadata(
kb_name=kb_name,
where={"source": "agent_os"}
)
if not results:
return {
"total_documents": 0,
"documents_by_type": {}
}
# Count by content type
type_counts = {}
for doc in results:
metadata = doc.get("metadata", {})
content_type = metadata.get("content_type", "unknown")
type_counts[content_type] = type_counts.get(content_type, 0) + 1
return {
"total_documents": len(results),
"documents_by_type": type_counts
}
except Exception as e:
logger.error(f"Failed to get profile stats for {kb_name}: {e}")
return {
"total_documents": 0,
"documents_by_type": {},
"error": str(e)
}
def search_by_type(
self,
kb_name: str,
content_type: AgentOSContentType,
query: Optional[str] = None,
limit: int = 10
) -> List[Dict[str, Any]]:
"""
Search Agent OS content by type.
Args:
kb_name: Name of the knowledge base
content_type: Type of content to search
query: Optional search query (if None, returns all of type)
limit: Maximum number of results
Returns:
List of matching documents
"""
try:
where_filter = {
"source": "agent_os",
"content_type": content_type.value
}
if query:
# Semantic search with type filter
embed_model = OllamaEmbedding(
model_name=Config.OLLAMA_EMBED_MODEL,
base_url=Config.OLLAMA_HOST
)
query_embedding = embed_model.get_text_embedding(query)
results = self.db_manager.query_documents(
kb_name=kb_name,
query_embedding=query_embedding,
n_results=limit,
where=where_filter
)
# Format query results
documents = []
for i in range(len(results.get("documents", []))):
documents.append({
"content": results["documents"][i],
"metadata": results["metadatas"][i],
"distance": results.get("distances", [0])[i]
})
else:
# Just get all of this type
results = self.db_manager.get_documents_by_metadata(
kb_name=kb_name,
where=where_filter,
limit=limit
)
# Format get results
documents = []
for doc in results:
documents.append({
"content": doc.get("content", ""),
"metadata": doc.get("metadata", {})
})
return documents
except Exception as e:
logger.error(f"Failed to search by type in {kb_name}: {e}")
return []

295
app/core/agent_os_parser.py Normal file
View file

@ -0,0 +1,295 @@
"""
Agent OS Parser Module
Parses Agent OS profile directory structures and extracts structured content
for ingestion into Code-Forge knowledge bases.
Agent OS Structure:
- standards/ - Coding standards and best practices
- agents/ - Agent configurations and skills
- workflows/ - Development workflow definitions
- commands/ - Command definitions
- product/ - Product vision and roadmap (optional)
- specs/ - Feature specifications (optional)
"""
import os
import yaml
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass
from enum import Enum
import logging
logger = logging.getLogger(__name__)
class AgentOSContentType(str, Enum):
"""Types of content in Agent OS profiles."""
STANDARD = "standard"
AGENT = "agent"
WORKFLOW = "workflow"
COMMAND = "command"
PRODUCT = "product"
SPEC = "spec"
UNKNOWN = "unknown"
@dataclass
class AgentOSDocument:
"""Represents a parsed Agent OS document."""
content_type: AgentOSContentType
title: str
content: str
file_path: str
metadata: Dict[str, Any]
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for storage."""
return {
"content_type": self.content_type.value,
"title": self.title,
"content": self.content,
"file_path": self.file_path,
"metadata": self.metadata
}
class AgentOSParser:
"""Parser for Agent OS profile directories."""
# Map directory names to content types
DIRECTORY_TYPE_MAP = {
"standards": AgentOSContentType.STANDARD,
"agents": AgentOSContentType.AGENT,
"workflows": AgentOSContentType.WORKFLOW,
"commands": AgentOSContentType.COMMAND,
"product": AgentOSContentType.PRODUCT,
"specs": AgentOSContentType.SPEC,
}
# Supported file extensions
SUPPORTED_EXTENSIONS = {".yml", ".yaml", ".md"}
def __init__(self):
"""Initialize the Agent OS parser."""
pass
def parse_directory(self, directory_path: str) -> List[AgentOSDocument]:
"""
Parse an Agent OS profile directory.
Args:
directory_path: Path to the Agent OS profile directory
Returns:
List of parsed AgentOSDocument objects
Raises:
ValueError: If directory doesn't exist or isn't a valid Agent OS profile
"""
path = Path(directory_path)
if not path.exists():
raise ValueError(f"Directory does not exist: {directory_path}")
if not path.is_dir():
raise ValueError(f"Path is not a directory: {directory_path}")
# Validate it's an Agent OS profile (has at least one expected subdirectory)
subdirs = [d.name for d in path.iterdir() if d.is_dir()]
valid_subdirs = set(self.DIRECTORY_TYPE_MAP.keys())
if not any(subdir in valid_subdirs for subdir in subdirs):
raise ValueError(
f"Not a valid Agent OS profile. Expected at least one of: {', '.join(valid_subdirs)}"
)
documents = []
# Parse each subdirectory
for subdir_name, content_type in self.DIRECTORY_TYPE_MAP.items():
subdir_path = path / subdir_name
if subdir_path.exists() and subdir_path.is_dir():
docs = self._parse_content_directory(subdir_path, content_type)
documents.extend(docs)
logger.info(f"Parsed {len(docs)} documents from {subdir_name}/")
logger.info(f"Total documents parsed: {len(documents)}")
return documents
def _parse_content_directory(
self,
directory: Path,
content_type: AgentOSContentType
) -> List[AgentOSDocument]:
"""
Parse all files in a content directory.
Args:
directory: Path to the content directory
content_type: Type of content in this directory
Returns:
List of parsed documents
"""
documents = []
# Recursively find all supported files
for file_path in directory.rglob("*"):
if file_path.is_file() and file_path.suffix in self.SUPPORTED_EXTENSIONS:
try:
doc = self._parse_file(file_path, content_type, directory)
if doc:
documents.append(doc)
except Exception as e:
logger.error(f"Failed to parse {file_path}: {e}")
return documents
def _parse_file(
self,
file_path: Path,
content_type: AgentOSContentType,
base_dir: Path
) -> Optional[AgentOSDocument]:
"""
Parse a single file.
Args:
file_path: Path to the file
content_type: Type of content
base_dir: Base directory for relative path calculation
Returns:
Parsed AgentOSDocument or None if parsing fails
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
raw_content = f.read()
# Determine file type and parse accordingly
if file_path.suffix in {".yml", ".yaml"}:
return self._parse_yaml_file(file_path, raw_content, content_type, base_dir)
elif file_path.suffix == ".md":
return self._parse_markdown_file(file_path, raw_content, content_type, base_dir)
except Exception as e:
logger.error(f"Error reading file {file_path}: {e}")
return None
def _parse_yaml_file(
self,
file_path: Path,
raw_content: str,
content_type: AgentOSContentType,
base_dir: Path
) -> Optional[AgentOSDocument]:
"""Parse a YAML file."""
try:
data = yaml.safe_load(raw_content)
if not data:
logger.warning(f"Empty YAML file: {file_path}")
return None
# Extract title (use 'name', 'title', or filename)
title = (
data.get("name") or
data.get("title") or
file_path.stem
)
# Extract metadata
metadata = {
"file_type": "yaml",
"relative_path": str(file_path.relative_to(base_dir)),
}
# Add common YAML fields to metadata
for key in ["description", "version", "category", "priority", "tags"]:
if key in data:
metadata[key] = data[key]
# Convert YAML to readable text for content
content = self._yaml_to_text(data, title)
return AgentOSDocument(
content_type=content_type,
title=title,
content=content,
file_path=str(file_path),
metadata=metadata
)
except yaml.YAMLError as e:
logger.error(f"YAML parsing error in {file_path}: {e}")
return None
def _parse_markdown_file(
self,
file_path: Path,
raw_content: str,
content_type: AgentOSContentType,
base_dir: Path
) -> Optional[AgentOSDocument]:
"""Parse a Markdown file."""
# Extract title from first heading or filename
title = file_path.stem
lines = raw_content.split('\n')
for line in lines:
if line.startswith('# '):
title = line[2:].strip()
break
metadata = {
"file_type": "markdown",
"relative_path": str(file_path.relative_to(base_dir)),
}
return AgentOSDocument(
content_type=content_type,
title=title,
content=raw_content,
file_path=str(file_path),
metadata=metadata
)
def _yaml_to_text(self, data: Dict[str, Any], title: str) -> str:
"""
Convert YAML data to readable text format.
This creates a structured text representation that's good for RAG.
"""
lines = [f"# {title}\n"]
def format_value(value: Any, indent: int = 0) -> List[str]:
"""Recursively format YAML values."""
result = []
prefix = " " * indent
if isinstance(value, dict):
for k, v in value.items():
result.append(f"{prefix}**{k}:**")
result.extend(format_value(v, indent + 1))
elif isinstance(value, list):
for item in value:
if isinstance(item, (dict, list)):
result.extend(format_value(item, indent))
else:
result.append(f"{prefix}- {item}")
else:
result.append(f"{prefix}{value}")
return result
# Format each top-level key
for key, value in data.items():
if key in ["name", "title"]: # Skip title fields (already in header)
continue
lines.append(f"\n## {key.replace('_', ' ').title()}")
lines.extend(format_value(value, 0))
return "\n".join(lines)

View file

@ -1,158 +0,0 @@
"""
ChromaDB client manager for knowledge base operations.
Implements singleton pattern for connection reuse.
"""
import logging
from typing import List, Optional
import chromadb
from chromadb.config import Settings
from app.core.config import Config
logger = logging.getLogger(__name__)
class ChromaManager:
"""Singleton manager for ChromaDB operations."""
_instance = None
_client = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if self._client is None:
self._initialize_client()
def _initialize_client(self):
"""Initialize ChromaDB HTTP client."""
try:
self._client = chromadb.HttpClient(
host=Config.CHROMA_HOST,
port=Config.CHROMA_PORT
)
logger.info(f"ChromaDB client initialized: {Config.get_chroma_url()}")
except Exception as e:
logger.error(f"Failed to initialize ChromaDB client: {e}")
raise
def get_client(self) -> chromadb.HttpClient:
"""Get the ChromaDB client instance."""
if self._client is None:
self._initialize_client()
return self._client
def list_collections(self) -> List[str]:
"""
List all collection names.
Returns:
List of collection names
"""
try:
collections = self._client.list_collections()
return [col.name for col in collections]
except Exception as e:
logger.error(f"Failed to list collections: {e}")
return []
def create_collection(self, name: str) -> bool:
"""
Create a new collection (knowledge base).
Args:
name: Collection name
Returns:
bool: True if successful
"""
try:
self._client.get_or_create_collection(name=name)
logger.info(f"Collection created: {name}")
return True
except Exception as e:
logger.error(f"Failed to create collection {name}: {e}")
return False
def delete_collection(self, name: str) -> bool:
"""
Delete a collection.
Args:
name: Collection name
Returns:
bool: True if successful
"""
try:
self._client.delete_collection(name=name)
logger.info(f"Collection deleted: {name}")
return True
except Exception as e:
logger.error(f"Failed to delete collection {name}: {e}")
return False
def get_collection(self, name: str) -> Optional[chromadb.Collection]:
"""
Get a collection by name.
Args:
name: Collection name
Returns:
Collection object or None if not found
"""
try:
return self._client.get_collection(name=name)
except Exception as e:
logger.warning(f"Collection {name} not found: {e}")
return None
def get_collection_count(self, name: str) -> int:
"""
Get the number of documents in a collection.
Args:
name: Collection name
Returns:
int: Document count (0 if collection not found)
"""
try:
collection = self.get_collection(name)
if collection:
return collection.count()
return 0
except Exception as e:
logger.error(f"Failed to get count for {name}: {e}")
return 0
def collection_exists(self, name: str) -> bool:
"""
Check if a collection exists.
Args:
name: Collection name
Returns:
bool: True if collection exists
"""
return name in self.list_collections()
# Global singleton instance
_chroma_manager = None
def get_chroma_manager() -> ChromaManager:
"""Get the global ChromaManager instance."""
global _chroma_manager
if _chroma_manager is None:
_chroma_manager = ChromaManager()
return _chroma_manager

View file

@ -5,7 +5,7 @@ Centralizes all application settings with environment variable support.
import os
from pathlib import Path
from typing import List
from typing import Dict, List
from dotenv import load_dotenv
@ -43,6 +43,34 @@ class Config:
TOP_K_RETRIEVAL: int = 5
RERANK_TOP_N: int = 3
# KB Type Configuration
DEFAULT_KB_TYPE: str = os.getenv("DEFAULT_KB_TYPE", "generic")
# Type-specific RAG strategy recommendations
# These are suggested defaults - users can override via UI
KB_TYPE_RAG_DEFAULTS: Dict[str, Dict[str, bool]] = {
"generic": {
"hybrid": False,
"rerank": False,
"agentic": False
},
"code": {
"hybrid": True, # Code benefits from keyword + semantic search
"rerank": True, # Reranking helps with code relevance
"agentic": False
},
"documentation": {
"hybrid": True, # Docs benefit from hybrid search
"rerank": True, # Reranking improves doc relevance
"agentic": False
},
"agent-os": {
"hybrid": True, # Agent OS specs benefit from hybrid search
"rerank": True, # Reranking for spec relevance
"agentic": True # Agentic RAG for complex spec queries
}
}
# Storage Configuration
UPLOAD_DIR: str = os.getenv("UPLOAD_DIR", "/workspace/data/uploads")

View file

@ -58,51 +58,39 @@ def check_ollama_health() -> Dict[str, any]:
}
def check_chroma_health() -> Dict[str, any]:
def check_postgres_health() -> Dict[str, any]:
"""
Check ChromaDB service health.
Check PostgreSQL service health.
Returns:
dict: Status information with 'status' and optional 'error'
"""
try:
# Use v2 API endpoint for health check
response = requests.get(
f"{Config.get_chroma_url()}/api/v2/auth/identity",
timeout=5
)
response.raise_for_status()
from app.core.pg_manager import get_pg_manager
pg_manager = get_pg_manager()
# Try to list collections as a health check
collections = pg_manager.list_collections()
return {
"status": "healthy",
"url": Config.get_chroma_url()
}
except requests.exceptions.ConnectionError as e:
logger.warning(f"ChromaDB connection failed: {e}")
return {
"status": "unhealthy",
"error": "Connection refused - is ChromaDB running?",
"url": Config.get_chroma_url()
}
except requests.exceptions.Timeout as e:
logger.warning(f"ChromaDB timeout: {e}")
return {
"status": "unhealthy",
"error": "Request timed out",
"url": Config.get_chroma_url()
"collections": len(collections)
}
except Exception as e:
logger.error(f"ChromaDB health check failed: {e}")
logger.warning(f"PostgreSQL connection failed: {e}")
return {
"status": "unhealthy",
"error": str(e),
"url": Config.get_chroma_url()
"error": "Connection refused - is PostgreSQL running?"
}
# Backwards compatibility alias
check_chroma_health = check_postgres_health
def wait_for_services(max_retries: int = 30, delay: int = 2) -> bool:
"""
Wait for both Ollama and ChromaDB services to become healthy.
Wait for both Ollama and PostgreSQL services to become healthy.
Args:
max_retries: Maximum number of retry attempts
@ -115,16 +103,16 @@ def wait_for_services(max_retries: int = 30, delay: int = 2) -> bool:
for attempt in range(1, max_retries + 1):
ollama_status = check_ollama_health()
chroma_status = check_chroma_health()
postgres_status = check_postgres_health()
if ollama_status["status"] == "healthy" and chroma_status["status"] == "healthy":
if ollama_status["status"] == "healthy" and postgres_status["status"] == "healthy":
logger.info(f"All services healthy after {attempt} attempts")
return True
logger.info(
f"Attempt {attempt}/{max_retries}: "
f"Ollama={ollama_status['status']}, "
f"ChromaDB={chroma_status['status']}"
f"PostgreSQL={postgres_status['status']}"
)
if attempt < max_retries:

View file

@ -14,7 +14,7 @@ from llama_index.core import Document, Settings
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.ollama import OllamaEmbedding
from app.core.chroma_manager import get_chroma_manager
from app.core.pg_manager import get_pg_manager
from app.core.config import Config
from app.core.markdown_preprocessor import preprocess_markdown
@ -141,9 +141,9 @@ def ingest_file(
base_url=Config.OLLAMA_HOST
)
# Get ChromaDB collection
chroma_manager = get_chroma_manager()
collection = chroma_manager.get_collection(collection_name)
# Get PostgreSQL collection
pg_manager = get_pg_manager()
collection = pg_manager.get_collection(collection_name)
if not collection:
return {

View file

@ -7,7 +7,8 @@ from collections import defaultdict
from datetime import datetime
from typing import Dict, List
from app.core.chroma_manager import get_chroma_manager
from app.core.pg_manager import get_pg_manager
from app.core.kb_types import KBType, get_kb_type_info, validate_kb_type
logger = logging.getLogger(__name__)
@ -15,10 +16,10 @@ logger = logging.getLogger(__name__)
def generate_tags(file_type: str) -> List[str]:
"""
Generate display tags based on file type.
Args:
file_type: File extension (e.g., '.py', '.md')
Returns:
List of tag strings
"""
@ -41,17 +42,17 @@ def generate_tags(file_type: str) -> List[str]:
".yaml": ["Config", "YAML"],
".yml": ["Config", "YAML"],
}
return tag_map.get(file_type.lower(), ["Document", "Unknown"])
def format_timestamp(iso_timestamp: str) -> str:
"""
Format ISO timestamp for display.
Args:
iso_timestamp: ISO format timestamp string
Returns:
Formatted string (e.g., "Updated: 10/22/2025")
"""
@ -66,33 +67,33 @@ def get_documents_metadata(collection_name: str) -> List[Dict[str, any]]:
"""
Get metadata for all documents in a collection.
Groups chunks by filename and aggregates metadata.
Args:
collection_name: Name of the collection
Returns:
List of document metadata dicts
"""
try:
chroma_manager = get_chroma_manager()
collection = chroma_manager.get_collection(collection_name)
if not collection:
pg_manager = get_pg_manager()
if not pg_manager.collection_exists(collection_name):
logger.warning(f"Collection {collection_name} not found")
return []
# Get all documents with metadata
results = collection.get(include=["metadatas"])
if not results or not results.get("metadatas"):
results = pg_manager.get_documents_by_metadata(collection_name, where={})
if not results:
return []
# Group chunks by filename
docs_by_filename = defaultdict(list)
for metadata in results["metadatas"]:
for doc in results:
metadata = doc.get("metadata", {})
filename = metadata.get("filename", "unknown")
docs_by_filename[filename].append(metadata)
# Aggregate metadata for each document
documents = []
for filename, chunks in docs_by_filename.items():
@ -100,7 +101,7 @@ def get_documents_metadata(collection_name: str) -> List[Dict[str, any]]:
first_chunk = chunks[0]
file_type = first_chunk.get("file_type", "")
upload_date = first_chunk.get("upload_date", "")
documents.append({
"filename": filename,
"file_type": file_type,
@ -109,15 +110,15 @@ def get_documents_metadata(collection_name: str) -> List[Dict[str, any]]:
"formatted_date": format_timestamp(upload_date),
"chunk_count": len(chunks)
})
# Sort by upload date (newest first)
documents.sort(
key=lambda x: x.get("upload_date", ""),
reverse=True
)
return documents
except Exception as e:
logger.error(f"Failed to get documents metadata: {e}")
return []
@ -125,51 +126,127 @@ def get_documents_metadata(collection_name: str) -> List[Dict[str, any]]:
def get_collection_stats(collection_name: str) -> Dict[str, any]:
"""
Get statistics for a collection.
Get statistics for a collection including KB type.
Args:
collection_name: Name of the collection
Returns:
dict: Statistics including total docs, chunks, last updated
dict: Statistics including total docs, chunks, last updated, and kb_type
"""
try:
chroma_manager = get_chroma_manager()
collection = chroma_manager.get_collection(collection_name)
if not collection:
pg_manager = get_pg_manager()
if not pg_manager.collection_exists(collection_name):
return {
"total_documents": 0,
"total_chunks": 0,
"last_updated": None
"last_updated": None,
"kb_type": KBType.GENERIC.value
}
# Get all metadata
results = collection.get(include=["metadatas"])
metadatas = results.get("metadatas", [])
# Get KB type from collection metadata
kb_metadata = pg_manager.get_collection_metadata(collection_name)
kb_type = kb_metadata.kb_type if kb_metadata else KBType.GENERIC
# Get all documents with metadata
results = pg_manager.get_documents_by_metadata(collection_name, where={})
# Count unique documents
unique_filenames = set(
m.get("filename", "") for m in metadatas
doc.get("metadata", {}).get("filename", "") for doc in results
)
# Get latest upload date
upload_dates = [
m.get("upload_date", "") for m in metadatas if m.get("upload_date")
doc.get("metadata", {}).get("upload_date", "")
for doc in results
if doc.get("metadata", {}).get("upload_date")
]
last_updated = max(upload_dates) if upload_dates else None
return {
"total_documents": len(unique_filenames),
"total_chunks": len(metadatas),
"last_updated": last_updated
"total_chunks": len(results),
"last_updated": last_updated,
"kb_type": kb_type.value if isinstance(kb_type, KBType) else kb_type
}
except Exception as e:
logger.error(f"Failed to get collection stats: {e}")
return {
"total_documents": 0,
"total_chunks": 0,
"last_updated": None
"last_updated": None,
"kb_type": KBType.GENERIC.value
}
def get_kb_type_badge(kb_type: KBType) -> str:
"""
Generate HTML badge for KB type display in UI.
Args:
kb_type: The KB type
Returns:
HTML string for badge display
Example:
>>> get_kb_type_badge(KBType.AGENT_OS)
'<span style="...">🤖 Agent OS Profile</span>'
"""
info = get_kb_type_info(kb_type)
return f'''
<span style="
background: {info.color}20;
color: {info.color};
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
border: 1px solid {info.color}40;
display: inline-block;
">
{info.icon} {info.name}
</span>
'''
def get_kb_type_summary() -> Dict[str, int]:
"""
Get summary of KB counts by type across all knowledge bases.
Returns:
Dict mapping KB type values to counts
Example:
{
"generic": 2,
"code": 5,
"documentation": 3,
"agent-os": 1
}
"""
try:
pg_manager = get_pg_manager()
collections = pg_manager.list_collections()
# Count by type
type_counts = {kb_type.value: 0 for kb_type in KBType}
for col in collections:
kb_type = col["metadata"].get("kb_type", KBType.GENERIC.value)
if kb_type in type_counts:
type_counts[kb_type] += 1
else:
# Handle unknown types
type_counts[KBType.GENERIC.value] += 1
return type_counts
except Exception as e:
logger.error(f"Failed to get KB type summary: {e}")
return {kb_type.value: 0 for kb_type in KBType}

231
app/core/kb_types.py Normal file
View file

@ -0,0 +1,231 @@
"""
Knowledge Base Type System for Code-Forge.
Defines KB types, metadata models, and type-specific configurations.
"""
from datetime import datetime
from enum import Enum
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
class KBType(str, Enum):
"""
Knowledge Base types supported by Code-Forge.
Each type represents a distinct category of knowledge with
specialized handling and retrieval strategies.
"""
GENERIC = "generic"
CODE = "code"
DOCUMENTATION = "documentation"
AGENT_OS = "agent-os"
class KBMetadata(BaseModel):
"""
Type-safe metadata model for Knowledge Bases.
Attributes:
kb_type: The type of knowledge base
description: Optional human-readable description
created_at: ISO timestamp of creation
tags: Optional list of custom tags
"""
kb_type: KBType = Field(default=KBType.GENERIC)
description: str = Field(default="")
created_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
tags: List[str] = Field(default_factory=list)
class Config:
use_enum_values = True # Serialize enums as their values
def to_dict(self) -> Dict[str, any]:
"""
Convert to dictionary for ChromaDB storage.
Note: ChromaDB only supports string, int, float, and bool metadata values.
Lists must be serialized to JSON strings.
"""
return {
"kb_type": self.kb_type.value if isinstance(self.kb_type, KBType) else self.kb_type,
"description": self.description,
"created_at": self.created_at,
"tags": ",".join(self.tags) if self.tags else "" # Serialize list as comma-separated string
}
@classmethod
def from_dict(cls, data: Dict[str, any]) -> "KBMetadata":
"""
Create from dictionary retrieved from ChromaDB.
Note: Deserializes comma-separated tag strings back to lists.
"""
tags_str = data.get("tags", "")
tags = [tag.strip() for tag in tags_str.split(",") if tag.strip()] if tags_str else []
return cls(
kb_type=KBType(data.get("kb_type", KBType.GENERIC.value)),
description=data.get("description", ""),
created_at=data.get("created_at", datetime.utcnow().isoformat()),
tags=tags
)
class KBTypeInfo(BaseModel):
"""
Display information for a KB type.
Attributes:
icon: Emoji icon for UI display
color: HSL color for Archon-inspired theming
name: Human-readable name
description: Detailed description of the type
use_cases: List of typical use cases
"""
icon: str
color: str
name: str
description: str
use_cases: List[str]
# Type-specific display configurations
KB_TYPE_INFO: Dict[KBType, KBTypeInfo] = {
KBType.GENERIC: KBTypeInfo(
icon="📦",
color="hsl(0, 0%, 60%)", # Gray
name="Generic",
description="General-purpose knowledge base for mixed content",
use_cases=[
"Mixed documentation and code",
"Personal notes and references",
"Unstructured knowledge collections"
]
),
KBType.CODE: KBTypeInfo(
icon="💻",
color="hsl(271, 91%, 65%)", # Archon purple
name="Code Repository",
description="Source code and technical implementation knowledge",
use_cases=[
"Application codebases",
"Library and framework code",
"Code examples and snippets",
"Technical implementation details"
]
),
KBType.DOCUMENTATION: KBTypeInfo(
icon="📚",
color="hsl(160, 84%, 39%)", # Archon green
name="Documentation",
description="Technical documentation, guides, and references",
use_cases=[
"API documentation",
"User guides and tutorials",
"Technical specifications",
"Architecture documentation"
]
),
KBType.AGENT_OS: KBTypeInfo(
icon="🤖",
color="hsl(330, 90%, 65%)", # Archon pink
name="Agent OS Profile",
description="Spec-driven development profiles with standards, specs, and workflows",
use_cases=[
"Coding standards and conventions",
"Product vision and roadmap",
"Feature specifications",
"Development workflows",
"Agent OS 3-layer context (Standards, Product, Specs)"
]
)
}
def get_kb_type_info(kb_type: KBType) -> KBTypeInfo:
"""
Get display information for a KB type.
Args:
kb_type: The KB type to get info for
Returns:
KBTypeInfo object with display metadata
Example:
>>> info = get_kb_type_info(KBType.AGENT_OS)
>>> print(f"{info.icon} {info.name}")
🤖 Agent OS Profile
"""
return KB_TYPE_INFO.get(kb_type, KB_TYPE_INFO[KBType.GENERIC])
def validate_kb_type(kb_type_str: str) -> Optional[KBType]:
"""
Validate and convert a string to KBType enum.
Args:
kb_type_str: String representation of KB type
Returns:
KBType enum if valid, None otherwise
Example:
>>> validate_kb_type("agent-os")
<KBType.AGENT_OS: 'agent-os'>
>>> validate_kb_type("invalid")
None
"""
try:
return KBType(kb_type_str)
except ValueError:
return None
def get_kb_type_display_name(kb_type: KBType) -> str:
"""
Get human-readable display name for a KB type.
Args:
kb_type: The KB type
Returns:
Display name string
Example:
>>> get_kb_type_display_name(KBType.AGENT_OS)
'🤖 Agent OS Profile'
"""
info = get_kb_type_info(kb_type)
return f"{info.icon} {info.name}"
def get_all_kb_types() -> List[KBType]:
"""
Get list of all available KB types.
Returns:
List of KBType enums
"""
return list(KBType)
def get_kb_type_choices() -> Dict[str, KBType]:
"""
Get KB type choices for UI dropdowns.
Returns:
Dict mapping display names to KBType enums
Example:
>>> choices = get_kb_type_choices()
>>> choices["🤖 Agent OS Profile"]
<KBType.AGENT_OS: 'agent-os'>
"""
return {
get_kb_type_display_name(kb_type): kb_type
for kb_type in get_all_kb_types()
}

348
app/core/pg_manager.py Normal file
View file

@ -0,0 +1,348 @@
"""
PostgreSQL + pgvector database manager for Code-Forge.
Replaces ChromaDB with a more robust, ACID-compliant solution.
"""
import os
import json
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
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
) -> Dict[str, Any]:
"""Create a new knowledge base."""
conn = self.get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"""
INSERT INTO knowledge_bases (name, kb_type, description, tags)
VALUES (%s, %s, %s, %s)
RETURNING id, name, kb_type, description, created_at, tags
""",
(name, kb_type.value, description, ",".join(tags) if tags else "")
)
result = cur.fetchone()
conn.commit()
return dict(result)
except psycopg2.IntegrityError:
conn.rollback()
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 all its documents."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
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, kb_type, description, created_at, tags, metadata
FROM knowledge_bases
ORDER BY created_at DESC
"""
)
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 "",
"tags": row["tags"] or "",
**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, tags, 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 "",
"tags": row["tags"] or "",
**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."""
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]
# Insert documents
with conn.cursor() as cur:
for doc, emb, meta, doc_id in zip(documents, embeddings, metadatas, ids):
cur.execute(
"""
INSERT INTO documents (kb_id, doc_id, content, embedding, metadata)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (kb_id, doc_id) DO UPDATE
SET content = EXCLUDED.content,
embedding = EXCLUDED.embedding,
metadata = EXCLUDED.metadata
""",
(kb_id, doc_id, doc, emb, Json(meta))
)
conn.commit()
finally:
self.return_connection(conn)
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."""
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:
return []
kb_id = result[0]
# Build WHERE clause for JSONB metadata
where_conditions = []
params = [kb_id]
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 = f"""
SELECT doc_id, content, metadata
FROM documents
WHERE kb_id = %s AND {where_clause}
"""
if limit:
query += f" LIMIT {limit}"
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(query, params)
results = cur.fetchall()
return [dict(row) 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."""
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_collection_count(self, kb_name: str) -> int:
"""Get document count for a knowledge base."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT COUNT(*) FROM documents d
JOIN knowledge_bases kb ON d.kb_id = kb.id
WHERE kb.name = %s
""",
(kb_name,)
)
return cur.fetchone()[0]
finally:
self.return_connection(conn)
def close(self):
"""Close all connections in the pool."""
if self.pool:
self.pool.closeall()
# 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

View file

@ -13,7 +13,8 @@ from llama_index.core.tools import QueryEngineTool
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.vector_stores.postgres import PGVectorStore
from sqlalchemy import make_url
# BM25 retriever - needs to be installed separately
try:
@ -22,7 +23,7 @@ try:
except ImportError:
HAS_BM25 = False
from app.core.chroma_manager import get_chroma_manager
from app.core.pg_manager import get_pg_manager
from app.core.config import Config
logger = logging.getLogger(__name__)
@ -33,10 +34,10 @@ class RAGEngine:
def __init__(self, collection_name: str):
"""
Initialize RAG engine for a specific knowledge base.
Initialize RAG engine for a specific knowledge base using PostgreSQL + pgvector.
Args:
collection_name: Name of the ChromaDB collection
collection_name: Name of the knowledge base
"""
self.collection_name = collection_name
@ -57,18 +58,54 @@ class RAGEngine:
base_url=Config.OLLAMA_HOST
)
# Get ChromaDB collection
chroma_manager = get_chroma_manager()
collection = chroma_manager.get_collection(collection_name)
if not collection:
# Verify KB exists
self.pg_manager = get_pg_manager()
if not self.pg_manager.collection_exists(collection_name):
raise ValueError(f"Collection {collection_name} not found")
# Create vector store and index
vector_store = ChromaVectorStore(chroma_collection=collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# Get KB ID for the vector store
conn = self.pg_manager.get_connection()
try:
with conn.cursor() as cur:
cur.execute("SELECT id FROM knowledge_bases WHERE name = %s", (collection_name,))
result = cur.fetchone()
if not result:
raise ValueError(f"Collection {collection_name} not found")
kb_id = result[0]
finally:
self.pg_manager.return_connection(conn)
# Build PostgreSQL connection URL
import os
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")
# Build connection string (handle empty password for trust auth)
if pg_password:
connection_string = f"postgresql+psycopg2://{pg_user}:{pg_password}@{pg_host}:{pg_port}/{pg_db}"
else:
connection_string = f"postgresql+psycopg2://{pg_user}@{pg_host}:{pg_port}/{pg_db}"
# Initialize PGVectorStore with llama_index
self.vector_store = PGVectorStore.from_params(
database=pg_db,
host=pg_host,
password=pg_password if pg_password else None,
port=int(pg_port),
user=pg_user,
table_name="documents",
embed_dim=768, # nomic-embed-text dimension
)
# Create storage context and index
storage_context = StorageContext.from_defaults(vector_store=self.vector_store)
# Load existing index from vector store
self.index = VectorStoreIndex.from_vector_store(
vector_store,
self.vector_store,
storage_context=storage_context
)
@ -86,11 +123,15 @@ class RAGEngine:
# Initialize BM25 retriever (for hybrid search)
if HAS_BM25:
try:
all_nodes = self.index.docstore.docs.values()
self.bm25_retriever = BM25Retriever.from_defaults(
nodes=list(all_nodes),
similarity_top_k=Config.TOP_K_RETRIEVAL
)
all_nodes = list(self.index.docstore.docs.values())
if all_nodes:
self.bm25_retriever = BM25Retriever.from_defaults(
nodes=all_nodes,
similarity_top_k=Config.TOP_K_RETRIEVAL
)
else:
logger.warning("No documents found for BM25 retriever")
self.bm25_retriever = None
except Exception as e:
logger.warning(f"Failed to initialize BM25 retriever: {e}")
self.bm25_retriever = None
@ -98,6 +139,32 @@ class RAGEngine:
logger.warning("BM25 retriever not available - llama-index-retrievers-bm25 not installed")
self.bm25_retriever = None
# Initialize reranker (optional)
try:
self.reranker = SentenceTransformerRerank(
model="cross-encoder/ms-marco-MiniLM-L-2-v2",
top_n=Config.TOP_K_RETRIEVAL
)
except Exception as e:
logger.warning(f"Failed to initialize reranker: {e}")
self.reranker = None
# Initialize sub-question engine for agentic RAG (optional)
try:
query_engine_tools = [
QueryEngineTool.from_defaults(
query_engine=self.query_engine,
name=collection_name,
description=f"Knowledge base: {collection_name}"
)
]
self.sub_question_engine = SubQuestionQueryEngine.from_defaults(
query_engine_tools=query_engine_tools
)
except Exception as e:
logger.warning(f"Failed to initialize sub-question engine: {e}")
self.sub_question_engine = None
# Initialize reranker
try:
self.reranker = SentenceTransformerRerank(
@ -239,7 +306,7 @@ class RAGEngine:
def _query_base(self, question: str) -> Dict[str, any]:
"""
Execute base vector search query.
Execute base vector search query using llama_index + PostgreSQL.
Args:
question: Query string

95
app/db/schema.sql Normal file
View file

@ -0,0 +1,95 @@
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Knowledge Bases (Collections)
CREATE TABLE IF NOT EXISTS knowledge_bases (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
kb_type VARCHAR(50) NOT NULL DEFAULT 'generic',
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
tags TEXT,
metadata JSONB DEFAULT '{}'::jsonb
);
CREATE INDEX idx_kb_name ON knowledge_bases(name);
CREATE INDEX idx_kb_type ON knowledge_bases(kb_type);
CREATE INDEX idx_kb_created_at ON knowledge_bases(created_at);
-- Documents
CREATE TABLE IF NOT EXISTS documents (
id SERIAL PRIMARY KEY,
kb_id INTEGER NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
doc_id VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
embedding vector(768), -- nomic-embed-text produces 768-dim vectors
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(kb_id, doc_id)
);
CREATE INDEX idx_doc_kb_id ON documents(kb_id);
CREATE INDEX idx_doc_doc_id ON documents(doc_id);
CREATE INDEX idx_doc_metadata ON documents USING GIN(metadata);
CREATE INDEX idx_doc_embedding ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
-- Agent OS specific content types
CREATE TABLE IF NOT EXISTS agent_os_content (
id SERIAL PRIMARY KEY,
kb_id INTEGER NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
content_type VARCHAR(50) NOT NULL, -- standard, agent, workflow, command, product, spec
name VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
file_path TEXT,
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(kb_id, content_type, name)
);
CREATE INDEX idx_aos_kb_id ON agent_os_content(kb_id);
CREATE INDEX idx_aos_content_type ON agent_os_content(content_type);
CREATE INDEX idx_aos_name ON agent_os_content(name);
CREATE INDEX idx_aos_metadata ON agent_os_content USING GIN(metadata);
-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger to auto-update updated_at
CREATE TRIGGER update_kb_updated_at BEFORE UPDATE ON knowledge_bases
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Helper function for cosine similarity search
CREATE OR REPLACE FUNCTION search_documents(
p_kb_id INTEGER,
p_query_embedding vector(768),
p_limit INTEGER DEFAULT 10,
p_metadata_filter JSONB DEFAULT NULL
)
RETURNS TABLE (
doc_id VARCHAR,
content TEXT,
similarity FLOAT,
metadata JSONB
) AS $$
BEGIN
RETURN QUERY
SELECT
d.doc_id,
d.content,
1 - (d.embedding <=> p_query_embedding) AS similarity,
d.metadata
FROM documents d
WHERE d.kb_id = p_kb_id
AND (p_metadata_filter IS NULL OR d.metadata @> p_metadata_filter)
ORDER BY d.embedding <=> p_query_embedding
LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;

File diff suppressed because it is too large Load diff

185
app/pages/0_Welcome.py Normal file
View file

@ -0,0 +1,185 @@
"""
Welcome page for Code-Forge - Default landing page
"""
import streamlit as st
from pathlib import Path
# Page config
st.set_page_config(
page_title="Welcome to Code-Forge",
page_icon="🔥",
layout="wide",
initial_sidebar_state="collapsed"
)
# Custom CSS - Archon-inspired badass styling
st.markdown("""
<style>
/* Hide Streamlit branding */
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
header {visibility: hidden;}
/* Hide sidebar completely */
section[data-testid="stSidebar"] {
display: none !important;
}
/* Hero section */
.hero {
background: linear-gradient(135deg,
hsl(271, 91%, 15%) 0%,
hsl(0, 0%, 0%) 50%,
hsl(160, 84%, 15%) 100%);
padding: 4rem 2rem;
border-radius: 20px;
margin-bottom: 2rem;
border: 2px solid hsl(271, 91%, 65%);
box-shadow: 0 0 40px hsla(271, 91%, 65%, 0.3);
position: relative;
overflow: hidden;
}
.hero::before {
content: "";
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, hsla(271, 91%, 65%, 0.1) 0%, transparent 70%);
animation: pulse 8s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.1); opacity: 0.8; }
}
.hero-title {
font-size: 4rem;
font-weight: 900;
background: linear-gradient(135deg, hsl(271, 91%, 65%), hsl(330, 90%, 65%), hsl(160, 84%, 39%));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
margin-bottom: 1rem;
position: relative;
z-index: 1;
text-shadow: 0 0 30px hsla(271, 91%, 65%, 0.5);
}
.hero-subtitle {
text-align: center;
color: hsl(160, 84%, 39%);
font-size: 1.5rem;
margin-bottom: 2rem;
position: relative;
z-index: 1;
}
.hero-emoji {
font-size: 6rem;
text-align: center;
margin-bottom: 1rem;
filter: drop-shadow(0 0 20px hsla(271, 91%, 65%, 0.8));
animation: float 3s ease-in-out infinite;
}
.hero-image {
max-width: 600px;
margin: 0 auto 2rem auto;
display: block;
border-radius: 15px;
filter: drop-shadow(0 0 30px hsla(271, 91%, 65%, 0.6));
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.feature-card {
background: linear-gradient(135deg, hsl(271, 91%, 10%), hsl(0, 0%, 5%));
padding: 1.5rem;
border-radius: 15px;
border: 1px solid hsl(271, 91%, 35%);
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.feature-card:hover {
border-color: hsl(271, 91%, 65%);
box-shadow: 0 0 20px hsla(271, 91%, 65%, 0.3);
transform: translateY(-5px);
}
.cta-button {
background: linear-gradient(135deg, hsl(271, 91%, 65%), hsl(330, 90%, 65%));
color: white;
padding: 1rem 2rem;
border-radius: 10px;
text-align: center;
font-size: 1.2rem;
font-weight: bold;
margin: 2rem auto;
cursor: pointer;
border: none;
box-shadow: 0 0 20px hsla(271, 91%, 65%, 0.5);
}
</style>
""", unsafe_allow_html=True)
# Hero Section
hero_path = Path("assets/hero.png")
st.markdown('<div class="hero">', unsafe_allow_html=True)
if hero_path.exists():
st.image(str(hero_path), use_container_width=False, output_format="PNG")
else:
st.markdown('<div class="hero-emoji">🔥</div>', unsafe_allow_html=True)
st.markdown("""
<div class="hero-title">CODE-FORGE</div>
<div class="hero-subtitle">Production-Grade RAG System with PostgreSQL + pgvector</div>
</div>
""", unsafe_allow_html=True)
# Navigation
nav1, nav2, nav3 = st.columns([1, 1, 6])
with nav1:
if st.button("🏠 Welcome", use_container_width=True, type="primary"):
st.switch_page("pages/0_Welcome.py")
with nav2:
if st.button("⚙️ Main", use_container_width=True):
st.switch_page("pages/1_Main.py")
st.markdown("<br>", unsafe_allow_html=True)
st.markdown("""
## 🚀 What is Code-Forge?
**Code-Forge** is a **localized, production-grade RAG system** that runs entirely on your machine.
### Key Features:
- 🗄️ **PostgreSQL + pgvector** - ACID-compliant vector database
- 🤖 **Ollama** - Local LLMs and embeddings
- 🔌 **MCP Integration** - Expose knowledge bases to AI agents
- 📚 **Multi-KB Architecture** - Isolated knowledge bases per project
- 🧠 **Advanced RAG** - Vector search, hybrid search, reranking, agentic RAG
## 🎯 Quick Start
1. Click **"⚙️ Main"** button above
2. Create a Knowledge Base from the left panel
3. Upload documents or import a directory
4. Start chatting with your knowledge base
---
**Ready to get started? Click the "⚙️ Main" button above!**
""")

851
app/pages/1_Main.py Normal file
View file

@ -0,0 +1,851 @@
"""
Code-Forge: Localized Multi-KB RAG System
Main Streamlit application with badass Archon-inspired UI.
"""
import logging
import os
from pathlib import Path
import streamlit as st
from app.core.pg_manager import get_pg_manager
from app.core.config import Config
from app.core.health import check_chroma_health, check_ollama_health
from app.core.ingestion import ingest_directory, ingest_file
from app.core.kb_metadata import get_documents_metadata, get_kb_type_badge, get_kb_type_summary
from app.core.kb_types import KBType, get_kb_type_choices, get_kb_type_info
from app.core.rag_engine import RAGEngine
from app.core.agent_os_ingestion import AgentOSIngestion
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Page configuration
st.set_page_config(
page_title="Code-Forge 🔥",
page_icon="🔥",
layout="wide",
initial_sidebar_state="collapsed"
)
# Initialize session state
if "selected_kb" not in st.session_state:
st.session_state.selected_kb = None
if "chat_history" not in st.session_state:
st.session_state.chat_history = []
if "rag_settings" not in st.session_state:
st.session_state.rag_settings = {
"hybrid": False,
"rerank": False,
"agentic": False
}
# Custom CSS with Archon aesthetic
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Material+Icons&display=swap');
* {
font-family: 'JetBrains Mono', monospace !important;
}
/* Hide Material Icons text in expanders and buttons */
[data-testid="stIconMaterial"] {
font-size: 0 !important;
width: 20px !important;
height: 20px !important;
display: inline-block !important;
position: relative !important;
}
/* Replace expander arrow with CSS */
[data-testid="stIconMaterial"]::before {
content: "▶" !important;
font-size: 14px !important;
color: hsl(271, 91%, 65%) !important;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
font-family: 'JetBrains Mono', monospace !important;
}
/* Rotate arrow when expander is open */
details[open] [data-testid="stIconMaterial"]::before {
transform: translate(-50%, -50%) rotate(90deg) !important;
}
/* Hide sidebar collapse button if it exists */
[data-testid="collapsedControl"] {
display: none !important;
}
/* Root app container */
.stApp {
background-color: #0E0E0E;
}
/* Fix main content area - prevent cutoff */
.main .block-container {
padding-top: 2rem !important;
padding-bottom: 2rem !important;
padding-left: 5rem !important;
padding-right: 5rem !important;
max-width: none !important;
}
.main {
background-color: #0E0E0E;
}
/* Ensure all content has proper spacing */
.element-container {
margin-left: 0 !important;
padding-left: 0 !important;
}
/* Hide Streamlit's sidebar completely */
section[data-testid="stSidebar"] {
display: none !important;
}
</style>
<style>
[data-testid="stSidebar"] code {
font-size: 11px !important;
word-break: break-all !important;
white-space: pre-wrap !important;
line-height: 1.4 !important;
}
[data-testid="stSidebar"] pre {
white-space: pre-wrap !important;
word-wrap: break-word !important;
overflow-x: hidden !important;
max-width: 100% !important;
font-size: 11px !important;
line-height: 1.4 !important;
}
[data-testid="stSidebar"] p {
overflow-wrap: break-word !important;
word-wrap: break-word !important;
font-size: 13px !important;
}
/* Larger text for headings in sidebar */
[data-testid="stSidebar"] h3 {
font-size: 16px !important;
}
[data-testid="stSidebar"] h4 {
font-size: 14px !important;
}
/* Fix expander headers in sidebar */
[data-testid="stSidebar"] .streamlit-expanderHeader {
font-size: 13px !important;
overflow-wrap: break-word !important;
word-wrap: break-word !important;
}
[data-testid="stSidebar"] details summary {
overflow-wrap: break-word !important;
word-wrap: break-word !important;
}
/* Fix any negative margins */
div[data-testid="column"] {
padding-left: 0 !important;
}
/* Status indicators */
.status-healthy {
color: hsl(160, 84%, 39%);
font-weight: bold;
text-shadow: 0 0 10px hsl(160, 84%, 39%);
}
.status-unhealthy {
color: #FF0000;
font-weight: bold;
text-shadow: 0 0 10px #FF0000;
}
/* Document cards */
.doc-card {
background: linear-gradient(135deg, #1A1A1A 0%, #0E0E0E 100%);
border: 2px solid hsl(330, 90%, 65%);
border-radius: 12px;
padding: 20px;
margin: 10px 0;
box-shadow: 0 8px 16px rgba(233, 30, 99, 0.3);
transition: all 0.3s ease;
}
.doc-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(233, 30, 99, 0.5);
border-color: hsl(271, 91%, 65%);
}
/* Tags */
.tag {
display: inline-block;
background: linear-gradient(135deg, hsl(271, 91%, 65%) 0%, hsl(330, 90%, 65%) 100%);
color: white;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: bold;
margin: 2px;
box-shadow: 0 2px 4px rgba(156, 39, 176, 0.3);
}
/* Buttons */
.stButton > button {
background: linear-gradient(135deg, #E91E63 0%, hsl(271, 91%, 65%) 100%);
color: white;
border: none;
border-radius: 8px;
font-weight: bold;
padding: 12px 24px;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(233, 30, 99, 0.3);
width: 100% !important;
max-width: 100% !important;
}
.stButton > button:hover {
box-shadow: 0 0 30px rgba(233, 30, 99, 0.8);
transform: scale(1.05);
}
/* Fix sidebar buttons to not overflow */
[data-testid="stSidebar"] .stButton > button {
font-size: 12px !important;
padding: 8px 12px !important;
white-space: normal !important;
word-wrap: break-word !important;
}
/* Chat messages */
.user-message {
background: linear-gradient(135deg, #E91E63 0%, hsl(271, 91%, 65%) 100%);
color: white;
padding: 16px;
border-radius: 12px;
margin: 12px 0;
box-shadow: 0 4px 8px rgba(233, 30, 99, 0.3);
}
.assistant-message {
background: #1A1A1A;
color: white;
padding: 16px;
border-radius: 12px;
margin: 12px 0;
border-left: 4px solid hsl(160, 84%, 39%);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
}
/* Header */
.main-header {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #0E0E0E 0%, #1A1A1A 100%);
border-bottom: 2px solid hsl(271, 91%, 65%);
margin-bottom: 20px;
}
.main-header h1 {
color: hsl(271, 91%, 65%);
text-shadow: 0 0 20px hsl(271, 91%, 65%);
}
.main-header p {
color: hsl(160, 84%, 39%);
}
</style>
""", unsafe_allow_html=True)
# Header
st.markdown("""
<div class="main-header">
<h1>🔥 Code-Forge</h1>
<p>Localized Multi-KB RAG System with MCP Integration</p>
</div>
""", unsafe_allow_html=True)
# Navigation
nav1, nav2, nav3 = st.columns([1, 1, 6])
with nav1:
if st.button("🏠 Welcome", use_container_width=True, key="nav_welcome"):
st.switch_page("pages/0_Welcome.py")
with nav2:
if st.button("⚙️ Main", use_container_width=True, key="nav_main", type="primary"):
st.switch_page("pages/1_Main.py")
st.divider()
# Create two-column layout: left panel + main content
left_col, main_col = st.columns([1, 3], gap="medium")
# Left Panel
with left_col:
st.markdown("## ⚙️ Settings")
# Connection Status
st.markdown("### 🔌 Connection Status")
ollama_status = check_ollama_health()
chroma_status = check_chroma_health()
if ollama_status["status"] == "healthy":
st.markdown('<p class="status-healthy">🟢 Ollama Connected</p>', unsafe_allow_html=True)
if ollama_status.get("models"):
st.caption(f"Models: {', '.join(ollama_status['models'][:3])}")
else:
st.markdown('<p class="status-unhealthy">🔴 Ollama Disconnected</p>', unsafe_allow_html=True)
st.caption(ollama_status.get("error", "Unknown error"))
if chroma_status["status"] == "healthy":
st.markdown('<p class="status-healthy">🟢 PostgreSQL Connected</p>', unsafe_allow_html=True)
else:
st.markdown('<p class="status-unhealthy">🔴 PostgreSQL Disconnected</p>', unsafe_allow_html=True)
st.caption(chroma_status.get("error", "Unknown error"))
st.divider()
# MCP Server Info
st.markdown("### 🔗 MCP Server")
mcp_url = f"http://localhost:{Config.MCP_SERVER_PORT}/mcp"
st.code(mcp_url, language="text")
st.caption("**Connection Command:**")
st.code(f"claude mcp add --transport http code-forge {mcp_url}", language="bash")
st.divider()
# Knowledge Base Selector
st.markdown("### 📚 Knowledge Base")
pg_manager = get_pg_manager()
collections = pg_manager.list_collections()
if collections:
# Optional type filter
type_summary = get_kb_type_summary()
filter_options = ["All Types"] + [
f"{get_kb_type_info(KBType(kb_type)).icon} {get_kb_type_info(KBType(kb_type)).name} ({count})"
for kb_type, count in type_summary.items() if count > 0
]
selected_filter = st.selectbox(
"Filter by Type",
options=filter_options,
key="kb_type_filter"
)
# Filter collections if needed
if selected_filter != "All Types":
# Extract the KB type value from the filter string
filter_type_value = None
for kb_type, count in type_summary.items():
type_info = get_kb_type_info(KBType(kb_type))
if f"{type_info.icon} {type_info.name}" in selected_filter:
filter_type_value = kb_type
break
if filter_type_value:
collections = [
col for col in collections
if col["metadata"].get("kb_type") == filter_type_value
]
# Build display options with type badges
kb_display_options = []
kb_name_map = {} # Map display string to actual name
for col in collections:
name = col["name"]
metadata = col["metadata"]
kb_type = KBType(metadata.get("kb_type", KBType.GENERIC.value))
type_info = get_kb_type_info(kb_type)
display_name = f"{type_info.icon} {name}"
kb_display_options.append(display_name)
kb_name_map[display_name] = name
selected_display = st.selectbox(
"Select KB",
kb_display_options,
key="kb_selector"
)
# Map back to actual KB name
st.session_state.selected_kb = kb_name_map[selected_display]
# Display selected KB metadata
selected_col = next(
(col for col in collections if col["name"] == st.session_state.selected_kb),
None
)
if selected_col:
metadata = selected_col["metadata"]
kb_type = KBType(metadata.get("kb_type", KBType.GENERIC.value))
# Show type badge
st.markdown(get_kb_type_badge(kb_type), unsafe_allow_html=True)
# Show description if available
description = metadata.get("description", "")
if description:
st.caption(description)
else:
st.warning("No knowledge bases found. Create one below!")
st.session_state.selected_kb = None
# Create new KB
with st.expander("➕ Create New KB"):
new_kb_name = st.text_input("KB Name", key="new_kb_name", placeholder="my-project")
# KB Type Selector
kb_type_choices = get_kb_type_choices()
selected_type_display = st.selectbox(
"KB Type",
options=kb_type_choices,
key="kb_type_selector",
help="Select the type of knowledge base to create"
)
# Extract the actual KBType from the display string
# Format is "🤖 Agent OS Profile" -> need to map back to KBType
type_map = {choice: kb_type for kb_type, choice in zip(KBType, kb_type_choices)}
selected_kb_type = type_map[selected_type_display]
# Show type-specific info
type_info = get_kb_type_info(selected_kb_type)
st.caption(f"**{type_info.description}**")
# Optional description
kb_description = st.text_area(
"Description (optional)",
key="kb_description",
placeholder="Brief description of this knowledge base...",
height=80
)
if st.button("Create", type="primary"):
if new_kb_name:
if pg_manager.create_collection(
name=new_kb_name,
kb_type=selected_kb_type,
description=kb_description
):
st.success(f"✅ Created {type_info.icon} **{new_kb_name}** ({type_info.name})")
st.rerun()
else:
st.error("Failed to create KB")
else:
st.error("Please enter a KB name")
st.divider()
# RAG Strategy Toggles
st.markdown("### 🎯 RAG Strategies")
st.session_state.rag_settings["hybrid"] = st.checkbox(
"🔀 Hybrid Search",
value=st.session_state.rag_settings["hybrid"],
help="**Hybrid Search**: Combines semantic vector search with BM25 keyword search for better recall. Uses reciprocal rank fusion to merge results. Best for queries with specific technical terms."
)
st.session_state.rag_settings["rerank"] = st.checkbox(
"🎯 Reranking",
value=st.session_state.rag_settings["rerank"],
help="**Reranking**: Re-scores retrieved chunks using a cross-encoder model (ms-marco-MiniLM) for higher precision. Keeps top 3 most relevant results. Improves answer quality but adds latency."
)
st.session_state.rag_settings["agentic"] = st.checkbox(
"🤖 Agentic RAG",
value=st.session_state.rag_settings["agentic"],
help="**Agentic RAG**: Breaks complex questions into sub-questions and answers them independently before synthesizing. Best for multi-part or analytical queries. Slower but more thorough."
)
# Main content area (right column)
with main_col:
# Main content tabs
tab1, tab2 = st.tabs(["📚 Knowledge Base", "💬 Chat"])
# Tab 1: Knowledge Base Management
with tab1:
st.markdown("## 📚 Knowledge Base Management")
if not st.session_state.selected_kb:
st.info("👈 Please select or create a knowledge base from the left panel")
else:
st.markdown(f"### Current KB: **{st.session_state.selected_kb}**")
st.markdown("---")
# Document Upload
doc_col1, doc_col2 = st.columns(2, gap="large")
with doc_col1:
st.markdown("#### 📤 Upload Documents")
st.caption("Choose files to upload to the knowledge base")
# Initialize upload counter in session state
if "upload_counter" not in st.session_state:
st.session_state.upload_counter = 0
# Use dynamic key to reset file uploader after upload
uploaded_files = st.file_uploader(
"Choose files",
accept_multiple_files=True,
type=[ext.replace(".", "") for ext in Config.SUPPORTED_FILE_TYPES],
label_visibility="collapsed",
key=f"file_uploader_{st.session_state.upload_counter}"
)
if uploaded_files and st.button("Upload Files", key="upload_files_btn", use_container_width=True):
try:
Config.ensure_upload_dir()
progress_bar = st.progress(0)
status_container = st.container()
for i, uploaded_file in enumerate(uploaded_files):
with status_container:
st.write(f"Processing: {uploaded_file.name}...")
# Save file
file_path = Path(Config.UPLOAD_DIR) / uploaded_file.name
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
# Ingest file
result = ingest_file(
str(file_path),
st.session_state.selected_kb,
uploaded_file.name
)
with status_container:
if result["status"] == "success":
st.success(f"✅ {uploaded_file.name}: {result['chunks']} chunks")
else:
st.error(f"❌ {uploaded_file.name}: {result.get('error', 'Unknown error')}")
progress_bar.progress((i + 1) / len(uploaded_files))
with status_container:
st.success("🎉 Upload complete! Refreshing document list...")
# Increment counter to reset file uploader on next render
st.session_state.upload_counter += 1
# Wait a moment before rerun so user can see the success message
import time
time.sleep(1.5)
st.rerun()
except Exception as e:
st.error(f"❌ Upload failed: {str(e)}")
import traceback
st.code(traceback.format_exc())
with doc_col2:
st.markdown("#### 📁 Import Directory")
st.caption("Import all supported files from a directory")
st.info("💡 **Tip:** Mount your directory as a volume in docker-compose.yml first. Paths are relative to `/workspace` in the container.")
dir_path = st.text_input(
"Directory Path",
placeholder="/workspace/mounted_docs",
help="Path must exist inside the Docker container",
label_visibility="collapsed"
)
if st.button("Import Directory", key="import_dir_btn", use_container_width=True):
if not dir_path:
st.error("Please enter a directory path")
else:
with st.spinner("Importing files..."):
results = ingest_directory(dir_path, st.session_state.selected_kb)
if not results:
st.warning("No supported files found in directory")
elif results[0].get("error") and "not found" in results[0].get("error", ""):
st.error(f"Directory not found: {dir_path}")
else:
success_count = sum(1 for r in results if r.get("status") == "success")
error_count = len(results) - success_count
if success_count > 0:
st.success(f"✅ Imported {success_count} file(s)")
if error_count > 0:
st.warning(f"⚠️ {error_count} file(s) failed")
# Show first 5 results
for result in results[:5]:
if result.get("status") == "success":
st.caption(f"✅ {result.get('filename', 'Unknown')}")
else:
st.caption(f"❌ {result.get('filename', 'Unknown')}: {result.get('error', 'Unknown error')}")
if len(results) > 5:
st.caption(f"... and {len(results) - 5} more")
st.rerun()
# Agent OS Profile Import (only for Agent OS KBs)
kb_metadata = pg_manager.get_collection_metadata(st.session_state.selected_kb)
kb_type_str = kb_metadata.get("kb_type", "generic")
if kb_type_str == "agent-os":
st.markdown("---")
st.markdown("#### 🤖 Import Agent OS Profile")
st.caption("Import an Agent OS profile directory with standards, agents, workflows, and commands")
st.info("💡 **Tip:** Mount your Agent OS profile directory as a volume in docker-compose.yml. The path should point to the profile root (containing standards/, agents/, etc.)")
agent_os_path = st.text_input(
"Agent OS Profile Path",
placeholder="/workspace/my-agent-os-profile",
help="Path to Agent OS profile directory (must exist inside the Docker container)",
key="agent_os_path_input"
)
if st.button("Import Agent OS Profile", key="import_agent_os_btn", use_container_width=True):
if not agent_os_path:
st.error("Please enter an Agent OS profile path")
else:
with st.spinner("Parsing and ingesting Agent OS profile..."):
try:
agent_os_ingestion = AgentOSIngestion(pg_manager)
stats = agent_os_ingestion.ingest_profile(
kb_name=st.session_state.selected_kb,
profile_path=agent_os_path
)
if stats.get("success"):
st.success(f"✅ Imported {stats['total_documents']} Agent OS documents!")
# Show breakdown by type
st.markdown("**Documents by type:**")
for content_type, count in stats.get("documents_by_type", {}).items():
type_emoji = {
"standard": "📋",
"agent": "🤖",
"workflow": "🔄",
"command": "⚡",
"product": "🎯",
"spec": "📝"
}.get(content_type, "📄")
st.caption(f"{type_emoji} {content_type}: {count}")
st.rerun()
else:
st.error("❌ Agent OS import failed")
if stats.get("errors"):
for error in stats["errors"]:
st.caption(f"⚠️ {error}")
except Exception as e:
st.error(f"❌ Agent OS import failed: {str(e)}")
import traceback
st.code(traceback.format_exc())
st.divider()
# Agent OS Content Stats (only for Agent OS KBs)
if kb_type_str == "agent-os":
agent_os_ingestion = AgentOSIngestion(pg_manager)
agent_os_stats = agent_os_ingestion.get_profile_stats(st.session_state.selected_kb)
if agent_os_stats.get("total_documents", 0) > 0:
st.markdown("#### 🤖 Agent OS Content")
# Display stats in columns
stat_cols = st.columns(len(agent_os_stats.get("documents_by_type", {})) or 1)
type_info = {
"standard": {"emoji": "📋", "name": "Standards"},
"agent": {"emoji": "🤖", "name": "Agents"},
"workflow": {"emoji": "🔄", "name": "Workflows"},
"command": {"emoji": "⚡", "name": "Commands"},
"product": {"emoji": "🎯", "name": "Product"},
"spec": {"emoji": "📝", "name": "Specs"}
}
for idx, (content_type, count) in enumerate(agent_os_stats.get("documents_by_type", {}).items()):
info = type_info.get(content_type, {"emoji": "📄", "name": content_type.title()})
with stat_cols[idx]:
st.metric(
label=f"{info['emoji']} {info['name']}",
value=count
)
st.divider()
# Document Grid
st.markdown("#### 📄 Documents")
docs = get_documents_metadata(st.session_state.selected_kb)
if not docs:
st.info("No documents in this knowledge base. Upload files to get started!")
else:
# Display in 3-column grid
cols = st.columns(3)
for i, doc in enumerate(docs):
with cols[i % 3]:
# File icon based on type
icon_map = {
".py": "🐍", ".js": "📜", ".jsx": "⚛️",
".ts": "📘", ".tsx": "⚛️", ".md": "📄",
".pdf": "📕", ".json": "📋", ".yaml": "⚙️"
}
icon = icon_map.get(doc["file_type"], "📄")
# Create card
tags_html = "".join([f'<span class="tag">{tag}</span>' for tag in doc["tags"]])
st.markdown(f"""
<div class="doc-card">
<div style="font-size: 24px;">{icon}</div>
<div style="font-weight: bold; margin: 8px 0;">{doc["filename"]}</div>
<div>{tags_html}</div>
<div style="font-size: 11px; color: #888; margin-top: 8px;">
{doc["formatted_date"]}<br>
{doc["chunk_count"]} chunks
</div>
</div>
""", unsafe_allow_html=True)
# Tab 2: Chat Interface
with tab2:
st.markdown("## 💬 Chat with Your Knowledge Base")
if not st.session_state.selected_kb:
st.info("👈 Please select a knowledge base from the left panel")
else:
# Display active strategies banner
active_strategies = []
if st.session_state.rag_settings["hybrid"]:
active_strategies.append("🔀 Hybrid Search")
if st.session_state.rag_settings["rerank"]:
active_strategies.append("🎯 Reranking")
if st.session_state.rag_settings["agentic"]:
active_strategies.append("🤖 Agentic RAG")
if active_strategies:
strategies_text = " + ".join(active_strategies)
st.markdown(f"""
<div style="background: linear-gradient(90deg, hsl(271 91% 65% / 0.15), hsl(160 84% 39% / 0.15));
border-left: 3px solid hsl(271 91% 65%);
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 16px;
font-family: 'JetBrains Mono', monospace;">
<strong>🎯 Active Retrieval Strategies:</strong> {strategies_text}
</div>
""", unsafe_allow_html=True)
else:
st.markdown(f"""
<div style="background: hsl(0 0% 10%);
border-left: 3px solid hsl(0 0% 30%);
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 16px;
font-family: 'JetBrains Mono', monospace;
color: hsl(0 0% 60%);">
<strong>📊 Mode:</strong> Standard Vector Search
</div>
""", unsafe_allow_html=True)
# Display chat history
for message in st.session_state.chat_history:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if message["role"] == "assistant" and message.get("sources"):
with st.expander("📚 View Sources"):
for i, source in enumerate(message["sources"][:3], 1):
st.markdown(f"**Source {i}:**")
st.code(source["text"][:200] + "..." if len(source["text"]) > 200 else source["text"])
if source.get("metadata"):
st.caption(f"From: {source['metadata'].get('filename', 'Unknown')}")
if source.get("score"):
st.caption(f"Score: {source['score']:.3f}")
st.divider()
# Chat input
if prompt := st.chat_input("Ask a question about your knowledge base..."):
# Add user message
st.session_state.chat_history.append({
"role": "user",
"content": prompt
})
# Display user message
with st.chat_message("user"):
st.markdown(prompt)
# Generate response
with st.chat_message("assistant"):
with st.spinner("Thinking..."):
try:
rag_engine = RAGEngine(st.session_state.selected_kb)
result = rag_engine.query(
prompt,
use_hybrid=st.session_state.rag_settings["hybrid"],
use_rerank=st.session_state.rag_settings["rerank"],
use_agentic=st.session_state.rag_settings["agentic"]
)
answer = result.get("answer", "No answer generated")
sources = result.get("sources", [])
st.markdown(answer)
# Add to history
st.session_state.chat_history.append({
"role": "assistant",
"content": answer,
"sources": sources
})
# Display sources
if sources:
with st.expander("📚 View Sources"):
for i, source in enumerate(sources[:3], 1):
st.markdown(f"**Source {i}:**")
st.code(source["text"][:200] + "..." if len(source["text"]) > 200 else source["text"])
if source.get("metadata"):
st.caption(f"From: {source['metadata'].get('filename', 'Unknown')}")
if source.get("score"):
st.caption(f"Score: {source['score']:.3f}")
st.divider()
except Exception as e:
error_msg = f"Error: {str(e)}"
st.error(error_msg)
st.session_state.chat_history.append({
"role": "assistant",
"content": error_msg
})
# Clear chat button
if st.session_state.chat_history and st.button("🗑️ Clear Chat"):
st.session_state.chat_history = []
st.rerun()

62
design-guide.md Normal file
View file

@ -0,0 +1,62 @@
# Code-Forge Design Guide: Enhanced High-Contrast Palette
## Concept
A dynamic, modern, and high-tech aesthetic, emphasizing extreme clarity and energetic innovation. The colors evoke digital craftsmanship and cutting-edge technology, with a deliberate focus on high contrast to ensure visual impact and accessibility.
## Primary Color Palette
Here are the core colors that define the Code-Forge brand, designed for maximum brightness and contrast:
1. ### **Deep Night (Background)**
* **Hex:** `#0A0E1A`
* **RGB:** `(10, 14, 26)`
* **Usage:** The foundational dark background color. This deep, almost black, blue-grey provides a dramatic canvas, making the vibrant foreground elements pop with intensity. Use for primary backgrounds, dark mode interfaces, and large informational sections.
2. ### **Electric Teal (Primary Accent)**
* **Hex:** `#00FFFF` (Pure Cyan)
* **RGB:** `(0, 255, 255)`
* **Usage:** The most prominent accent color. This intensely bright and pure cyan is used for key interactive elements, primary iconography, active states, glowing outlines, circuit patterns, and significant graphical components. Its high luminescence creates a striking contrast against "Deep Night."
3. ### **Blaze Orange (Secondary Accent)**
* **Hex:** `#FF8C00` (Dark Orange, but used at full saturation for brilliance)
* **RGB:** `(255, 140, 0)`
* **Usage:** A vibrant, saturated orange that provides a warm and energetic counterpoint to "Electric Teal." Use for call-to-action buttons, warning indicators, secondary highlights, "spark" effects, important data points, and to draw immediate attention.
4. ### **Pure White (Text & UI)**
* **Hex:** `#FFFFFF`
* **RGB:** `(255, 255, 255)`
* **Usage:** The default color for primary text, headings, essential labels, and clean UI elements. Offers the highest possible readability and contrast against "Deep Night."
## Secondary/Support Palette (Optional but Recommended)
These colors can be used for supplementary elements, gradients, or to add subtle depth without detracting from the primary palette's vibrancy.
1. ### **Cool Blue (Subtle Glow/Gradient)**
* **Hex:** `#4D94FF`
* **RGB:** `(77, 148, 255)`
* **Usage:** For background gradients, subtle glowing effects, or to create depth behind "Electric Teal" elements. Use sparingly to enhance rather than compete with the primary accents.
2. ### **Light Grey (Subtle Text/Dividers)**
* **Hex:** `#C0C0C0`
* **RGB:** `(192, 192, 192)`
* **Usage:** For less critical secondary text, disabled states, subtle dividers, borders, or backgrounds for minor information sections. Provides a softer, yet still clear, contrast to "Deep Night."
## Accessibility & Contrast Ratios
The primary palette is engineered for optimal contrast to ensure excellent readability and accessibility.
* **Deep Night** vs. **Electric Teal**: Excellent contrast (WCAG AAA compliant).
* **Deep Night** vs. **Blaze Orange**: Excellent contrast (WCAG AAA compliant).
* **Deep Night** vs. **Pure White**: Maximum contrast (WCAG AAA compliant), ideal for primary text.
## Usage Guidelines
* **Backgrounds:** Exclusively use "Deep Night" as the primary canvas to ensure maximum impact of the accent colors.
* **Primary Elements & Interactivity:** "Electric Teal" should highlight key interactive components, active states, and core branding visuals.
* **Emphasis & Alerts:** "Blaze Orange" is reserved for drawing immediate attention, warnings, confirmations, or energetic visual cues.
* **Text Hierarchy:** "Pure White" for main content and headings. "Light Grey" for supplementary information or less emphasized text.
* **Glows & Effects:** Embrace the vibrancy! Utilize subtle, expansive glows and light trails in "Electric Teal" and "Blaze Orange" to amplify the high-tech, energetic feel, similar to neon signage.
* **Gradients:** When used, gradients should smoothly transition between "Electric Teal" and "Cool Blue," or "Blaze Orange" for dramatic highlights, always respecting the high contrast against the "Deep Night" background.
* **Iconography:** Icons should primarily be "Electric Teal" or "Pure White" against "Deep Night," with "Blaze Orange" for special status icons.
---

View file

@ -7,7 +7,7 @@ services:
ports:
- "11434:11434"
volumes:
- ollama_models:/root/.ollama
- ./data/ollama:/root/.ollama
environment:
- OLLAMA_NUM_PARALLEL=1
- OLLAMA_MAX_LOADED_MODELS=1
@ -16,27 +16,13 @@ services:
- code-forge-network
restart: unless-stopped
chromadb:
image: chromadb/chroma:latest
container_name: code-forge-chromadb
ports:
- "8000:8000"
volumes:
- chroma_data:/chroma/chroma
environment:
- IS_PERSISTENT=TRUE
- ANONYMIZED_TELEMETRY=FALSE
networks:
- code-forge-network
restart: unless-stopped
app:
build:
context: .
dockerfile: Dockerfile
container_name: code-forge-app
ports:
- "8501:8501" # Streamlit UI
- "8501:8501" # Streamlit UI (Legacy)
- "8051:8051" # MCP Server
volumes:
- ./app:/workspace/app
@ -46,15 +32,36 @@ services:
# - /path/to/your/docs:/workspace/mounted_docs:ro
environment:
- OLLAMA_HOST=http://ollama:11434
- CHROMA_HOST=chromadb
- CHROMA_PORT=8000
- POSTGRES_HOST=host.docker.internal
- POSTGRES_PORT=5432
- POSTGRES_DB=codeforge
- POSTGRES_USER=${USER}
- POSTGRES_PASSWORD=
- DATABASE_URL=postgresql://${USER}@host.docker.internal:5432/codeforge
- MCP_SERVER_HOST=0.0.0.0
- MCP_SERVER_PORT=8051
networks:
- code-forge-network
depends_on:
- ollama
- chromadb
restart: unless-stopped
frontend:
image: node:20-alpine
container_name: code-forge-frontend
working_dir: /app
ports:
- "5173:5173" # Vite dev server
volumes:
- ./frontend:/app
- /app/node_modules # Prevent node_modules from being overwritten
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
environment:
- VITE_API_URL=http://localhost:8051
networks:
- code-forge-network
depends_on:
- app
restart: unless-stopped
networks:
@ -62,7 +69,5 @@ networks:
driver: bridge
volumes:
ollama_models:
chroma_data:
uploads:

24
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

73
frontend/README.md Normal file
View file

@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

23
frontend/eslint.config.js Normal file
View file

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
frontend/index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/assets/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Code-Forge | Localized Multi-KB RAG System</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6227
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

42
frontend/package.json Normal file
View file

@ -0,0 +1,42 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.90.5",
"@types/react-syntax-highlighter": "^15.5.13",
"axios": "^1.12.2",
"framer-motion": "^12.23.24",
"lucide-react": "^0.546.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.9.4",
"react-syntax-highlighter": "^16.0.0"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
"autoprefixer": "^10.4.21",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
"vite": "^7.1.7"
}
}

View file

@ -0,0 +1,7 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

1
frontend/public/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

29
frontend/src/App.tsx Normal file
View file

@ -0,0 +1,29 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Welcome from './pages/Welcome';
import MainApp from './pages/MainApp';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Routes>
<Route path="/" element={<Welcome />} />
<Route path="/app" element={<MainApp />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
</QueryClientProvider>
);
}
export default App;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,225 @@
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Send, Bot, User, Loader2, Copy, CheckCircle } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { chat, type ChatResponse } from '../lib/api';
import ReactMarkdown from 'react-markdown';
interface ChatInterfaceProps {
kbName: string;
useHybrid: boolean;
useRerank: boolean;
useAgentic: boolean;
}
interface Message {
role: 'user' | 'assistant';
content: string;
sources?: any[];
}
export default function ChatInterface({ kbName, useHybrid, useRerank, useAgentic }: ChatInterfaceProps) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const chatMutation = useMutation({
mutationFn: (query: string) => chat(kbName, query, useHybrid, useRerank, useAgentic),
onSuccess: (data: ChatResponse) => {
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: data.answer || data.error || 'No response',
sources: data.sources,
},
]);
},
});
const handleSend = () => {
if (!input.trim() || chatMutation.isPending) return;
const userMessage: Message = { role: 'user', content: input };
setMessages((prev) => [...prev, userMessage]);
setInput('');
chatMutation.mutate(input);
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const copyToClipboard = (text: string, index: number) => {
navigator.clipboard.writeText(text);
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
};
return (
<div className="flex flex-col h-full">
{/* Messages Area */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Bot className="w-16 h-16 text-electric-teal/50 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-light-grey mb-2">Start a Conversation</h2>
<p className="text-light-grey">Ask anything about your knowledge base</p>
<div className="mt-6 space-y-2">
<div className="text-sm text-light-grey">
<strong className="text-electric-teal">Active Settings:</strong>
</div>
<div className="flex gap-2 justify-center flex-wrap">
{useHybrid && (
<span className="px-3 py-1 bg-electric-teal/20 border border-electric-teal/50 rounded-full text-xs">
Hybrid Search
</span>
)}
{useRerank && (
<span className="px-3 py-1 bg-cool-blue/20 border border-cool-blue/50 rounded-full text-xs">
Reranking
</span>
)}
{useAgentic && (
<span className="px-3 py-1 bg-blaze-orange/20 border border-blaze-orange/50 rounded-full text-xs">
Agentic RAG
</span>
)}
{!useHybrid && !useRerank && !useAgentic && (
<span className="px-3 py-1 bg-light-grey/20 border border-light-grey/50 rounded-full text-xs">
Basic Vector Search
</span>
)}
</div>
</div>
</div>
</div>
) : (
<AnimatePresence>
{messages.map((message, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className={`flex gap-4 ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
{message.role === 'assistant' && (
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-electric-teal/20 flex items-center justify-center">
<Bot className="w-6 h-6 text-electric-teal" />
</div>
)}
<div
className={`max-w-3xl rounded-2xl p-4 ${
message.role === 'user'
? 'bg-gradient-to-r from-electric-teal to-cool-blue text-deep-night'
: 'bg-cool-blue/10 border border-electric-teal/30'
}`}
>
<div className="prose prose-invert max-w-none">
{message.role === 'assistant' ? (
<ReactMarkdown>{message.content}</ReactMarkdown>
) : (
<p className="m-0">{message.content}</p>
)}
</div>
{message.role === 'assistant' && (
<div className="mt-3 flex items-center gap-2">
<button
onClick={() => copyToClipboard(message.content, index)}
className="text-xs px-3 py-1 rounded bg-electric-teal/20 hover:bg-electric-teal/30 transition-colors flex items-center gap-1"
>
{copiedIndex === index ? (
<>
<CheckCircle className="w-3 h-3" />
Copied!
</>
) : (
<>
<Copy className="w-3 h-3" />
Copy
</>
)}
</button>
{message.sources && message.sources.length > 0 && (
<span className="text-xs text-light-grey">
{message.sources.length} source{message.sources.length > 1 ? 's' : ''}
</span>
)}
</div>
)}
</div>
{message.role === 'user' && (
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-blaze-orange/20 flex items-center justify-center">
<User className="w-6 h-6 text-blaze-orange" />
</div>
)}
</motion.div>
))}
</AnimatePresence>
)}
{chatMutation.isPending && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="flex gap-4"
>
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-electric-teal/20 flex items-center justify-center">
<Bot className="w-6 h-6 text-electric-teal" />
</div>
<div className="bg-cool-blue/10 border border-electric-teal/30 rounded-2xl p-4">
<Loader2 className="w-5 h-5 text-electric-teal animate-spin" />
</div>
</motion.div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="border-t border-electric-teal/30 p-6 bg-deep-night">
<div className="flex gap-4">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Ask a question about your knowledge base..."
className="input flex-1 resize-none h-12"
rows={1}
/>
<button
onClick={handleSend}
disabled={!input.trim() || chatMutation.isPending}
className="btn-primary px-6 flex items-center gap-2"
>
{chatMutation.isPending ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<>
<Send className="w-5 h-5" />
Send
</>
)}
</button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,262 @@
import { motion } from 'framer-motion';
import { Database, Upload, MessageSquare, Settings, Zap, Code, FileText, BookOpen } from 'lucide-react';
export default function HelpDocs() {
return (
<div className="space-y-6 max-w-5xl">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<h1 className="text-3xl font-bold gradient-text mb-2">Code-Forge Documentation</h1>
<p className="text-light-grey">Everything you need to know about using Code-Forge</p>
</motion.div>
{/* Quick Start */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="card"
>
<h2 className="text-2xl font-bold text-electric-teal mb-4 flex items-center gap-2">
<Zap className="w-6 h-6" />
Quick Start
</h2>
<div className="space-y-4 text-light-grey">
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-electric-teal/20 flex items-center justify-center text-electric-teal font-bold">1</div>
<div>
<h3 className="font-semibold text-white mb-1">Create a Knowledge Base</h3>
<p>Click the <span className="text-electric-teal">+</span> button in the sidebar to create a new KB. Choose the appropriate type for your content.</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-electric-teal/20 flex items-center justify-center text-electric-teal font-bold">2</div>
<div>
<h3 className="font-semibold text-white mb-1">Upload Documents</h3>
<p>Go to KB Management tab and drag & drop files or click to browse. Supports text, markdown, PDF, code files, and more.</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-electric-teal/20 flex items-center justify-center text-electric-teal font-bold">3</div>
<div>
<h3 className="font-semibold text-white mb-1">Start Chatting</h3>
<p>Switch to the Chat tab and ask questions about your documents. The AI will retrieve relevant context and provide answers.</p>
</div>
</div>
</div>
</motion.div>
{/* KB Types */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="card"
>
<h2 className="text-2xl font-bold text-electric-teal mb-4 flex items-center gap-2">
<Database className="w-6 h-6" />
Knowledge Base Types
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-cool-blue/10 rounded-lg border border-electric-teal/20">
<div className="flex items-center gap-2 mb-2">
<FileText className="w-5 h-5 text-electric-teal" />
<h3 className="font-bold text-white">Generic</h3>
</div>
<p className="text-sm text-light-grey mb-2">General-purpose knowledge base for mixed content types.</p>
<p className="text-xs text-light-grey"><strong className="text-electric-teal">Use for:</strong> Random notes, mixed documents, general reference material</p>
</div>
<div className="p-4 bg-cool-blue/10 rounded-lg border border-electric-teal/20">
<div className="flex items-center gap-2 mb-2">
<Code className="w-5 h-5 text-electric-teal" />
<h3 className="font-bold text-white">Code</h3>
</div>
<p className="text-sm text-light-grey mb-2">Optimized for source code with better chunking and structure preservation.</p>
<p className="text-xs text-light-grey"><strong className="text-electric-teal">Use for:</strong> Codebases, libraries, API implementations, code examples</p>
</div>
<div className="p-4 bg-cool-blue/10 rounded-lg border border-electric-teal/20">
<div className="flex items-center gap-2 mb-2">
<BookOpen className="w-5 h-5 text-electric-teal" />
<h3 className="font-bold text-white">Documentation</h3>
</div>
<p className="text-sm text-light-grey mb-2">Optimized for technical documentation with enhanced markdown processing.</p>
<p className="text-xs text-light-grey"><strong className="text-electric-teal">Use for:</strong> MCP docs, API docs, tutorials, guides, technical specs</p>
</div>
<div className="p-4 bg-cool-blue/10 rounded-lg border border-electric-teal/20">
<div className="flex items-center gap-2 mb-2">
<Settings className="w-5 h-5 text-electric-teal" />
<h3 className="font-bold text-white">Agent OS</h3>
</div>
<p className="text-sm text-light-grey mb-2">Special type for Agent OS YAML profiles with structured metadata extraction.</p>
<p className="text-xs text-light-grey"><strong className="text-electric-teal">Use for:</strong> Agent OS configuration files only</p>
</div>
</div>
</motion.div>
{/* RAG Settings */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="card"
>
<h2 className="text-2xl font-bold text-electric-teal mb-4 flex items-center gap-2">
<Settings className="w-6 h-6" />
RAG Settings Explained
</h2>
<div className="space-y-4">
<div className="p-4 bg-cool-blue/10 rounded-lg border border-electric-teal/20">
<h3 className="font-bold text-white mb-2">Hybrid Search (Vector + BM25)</h3>
<p className="text-sm text-light-grey mb-2">Combines semantic vector search with keyword-based BM25 search for better retrieval.</p>
<p className="text-xs text-light-grey"><strong className="text-blaze-orange">When to use:</strong> When you need both semantic understanding AND exact keyword matching. Great for technical queries.</p>
</div>
<div className="p-4 bg-cool-blue/10 rounded-lg border border-electric-teal/20">
<h3 className="font-bold text-white mb-2">Reranking</h3>
<p className="text-sm text-light-grey mb-2">Re-scores retrieved documents using a cross-encoder model for higher accuracy.</p>
<p className="text-xs text-light-grey"><strong className="text-blaze-orange">When to use:</strong> When you need the most relevant results and can afford slightly slower queries. Improves precision.</p>
</div>
<div className="p-4 bg-cool-blue/10 rounded-lg border border-electric-teal/20">
<h3 className="font-bold text-white mb-2">Agentic RAG</h3>
<p className="text-sm text-light-grey mb-2">Uses multi-step reasoning with an LLM agent to iteratively refine queries and answers.</p>
<p className="text-xs text-light-grey"><strong className="text-blaze-orange">When to use:</strong> For complex questions requiring multiple retrieval steps or reasoning. Slower but more intelligent.</p>
</div>
<div className="p-4 bg-electric-teal/10 rounded-lg border border-electric-teal/30">
<p className="text-sm text-white"><strong>💡 Pro Tip:</strong> Start with basic vector search, then enable Hybrid + Rerank for better results. Use Agentic RAG for complex research questions.</p>
</div>
</div>
</motion.div>
{/* File Upload */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="card"
>
<h2 className="text-2xl font-bold text-electric-teal mb-4 flex items-center gap-2">
<Upload className="w-6 h-6" />
Supported File Types
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm text-light-grey">
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.txt</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.md</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.pdf</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.py</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.js</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.ts</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.tsx</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.jsx</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.json</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.yaml</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.html</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.css</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.cpp</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.java</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.go</div>
<div className="p-2 bg-cool-blue/10 rounded border border-electric-teal/20 text-center">.rs</div>
</div>
<p className="text-sm text-light-grey mt-4">And many more! Code-Forge automatically extracts text from PDFs and processes markdown with enhanced structure preservation.</p>
</motion.div>
{/* MCP Integration */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="card"
>
<h2 className="text-2xl font-bold text-electric-teal mb-4 flex items-center gap-2">
<MessageSquare className="w-6 h-6" />
MCP Integration
</h2>
<p className="text-light-grey mb-4">Code-Forge exposes 12 MCP tools for AI agents to interact with your knowledge bases:</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm mb-6">
<div className="p-3 bg-cool-blue/10 rounded border border-electric-teal/20">
<code className="text-electric-teal">list_knowledge_bases</code>
</div>
<div className="p-3 bg-cool-blue/10 rounded border border-electric-teal/20">
<code className="text-electric-teal">create_knowledge_base</code>
</div>
<div className="p-3 bg-cool-blue/10 rounded border border-electric-teal/20">
<code className="text-electric-teal">delete_knowledge_base</code>
</div>
<div className="p-3 bg-cool-blue/10 rounded border border-electric-teal/20">
<code className="text-electric-teal">ingest_document</code>
</div>
<div className="p-3 bg-cool-blue/10 rounded border border-electric-teal/20">
<code className="text-electric-teal">ingest_directory</code>
</div>
<div className="p-3 bg-cool-blue/10 rounded border border-electric-teal/20">
<code className="text-electric-teal">query_knowledge_base</code>
</div>
<div className="p-3 bg-cool-blue/10 rounded border border-electric-teal/20">
<code className="text-electric-teal">get_kb_stats</code>
</div>
<div className="p-3 bg-cool-blue/10 rounded border border-electric-teal/20">
<code className="text-electric-teal">get_standards</code>
</div>
</div>
<div className="space-y-4">
<div className="p-4 bg-electric-teal/10 rounded-lg border border-electric-teal/30">
<p className="text-sm text-white mb-2"><strong>MCP Endpoint:</strong> <code className="text-electric-teal">http://localhost:8051/mcp</code></p>
</div>
<div className="p-4 bg-blaze-orange/10 rounded-lg border border-blaze-orange/30">
<h3 className="font-bold text-white mb-3 flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-blaze-orange" />
Add to Claude Desktop
</h3>
<div className="mb-4">
<p className="text-sm text-white mb-2"><strong>Option 1: Using CLI (Recommended)</strong></p>
<p className="text-sm text-light-grey mb-3">Run this command in your terminal:</p>
<div className="bg-deep-night rounded-lg p-4 border border-electric-teal/30">
<code className="text-electric-teal text-sm">npx @modelcontextprotocol/inspector http://localhost:8051/mcp</code>
</div>
<p className="text-xs text-light-grey mt-2">Or add directly to Claude Desktop:</p>
<div className="bg-deep-night rounded-lg p-4 border border-electric-teal/30 mt-2">
<code className="text-electric-teal text-sm">claude mcp add code-forge http://localhost:8051/mcp</code>
</div>
</div>
<div className="mb-4">
<p className="text-sm text-white mb-2"><strong>Option 2: Manual Configuration</strong></p>
<p className="text-sm text-light-grey mb-3">Edit your Claude Desktop config file:</p>
<div className="bg-deep-night rounded-lg p-4 border border-blaze-orange/30 mb-3">
<p className="text-xs text-light-grey mb-2"><strong>macOS:</strong> <code className="text-electric-teal">~/Library/Application Support/Claude/claude_desktop_config.json</code></p>
<p className="text-xs text-light-grey"><strong>Windows:</strong> <code className="text-electric-teal">%APPDATA%\Claude\claude_desktop_config.json</code></p>
</div>
<div className="bg-deep-night rounded-lg p-4 border border-electric-teal/30 overflow-x-auto">
<pre className="text-xs text-electric-teal">
{`{
"mcpServers": {
"code-forge": {
"url": "http://localhost:8051/mcp"
}
}
}`}
</pre>
</div>
</div>
<p className="text-xs text-light-grey">
<strong className="text-blaze-orange">Note:</strong> Restart Claude Desktop after adding the configuration. The MCP server must be running (docker-compose up).
</p>
</div>
</div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,261 @@
import { useState, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { FileText, BarChart3, Clock, Database, Upload, CheckCircle, XCircle } from 'lucide-react';
import { getKBStats, listDocuments, uploadDocument, type KBStats } from '../lib/api';
interface KBManagementProps {
kbName: string;
}
export default function KBManagement({ kbName }: KBManagementProps) {
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [uploadStatus, setUploadStatus] = useState<'idle' | 'uploading' | 'success' | 'error'>('idle');
const [uploadMessage, setUploadMessage] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
const queryClient = useQueryClient();
// Fetch KB stats
const { data: stats, isLoading: statsLoading } = useQuery({
queryKey: ['kb-stats', kbName],
queryFn: () => getKBStats(kbName),
enabled: !!kbName,
});
// Fetch documents
const { data: documents = [], isLoading: docsLoading } = useQuery({
queryKey: ['kb-documents', kbName],
queryFn: () => listDocuments(kbName),
enabled: !!kbName,
});
// Upload mutation
const uploadMutation = useMutation({
mutationFn: (file: File) => uploadDocument(kbName, file),
onSuccess: (data) => {
setUploadStatus('success');
setUploadMessage(`✅ Uploaded ${data.filename} (${data.chunks} chunks)`);
queryClient.invalidateQueries({ queryKey: ['kb-stats', kbName] });
queryClient.invalidateQueries({ queryKey: ['kb-documents', kbName] });
setTimeout(() => {
setUploadStatus('idle');
setSelectedFiles([]);
}, 3000);
},
onError: (error: any) => {
setUploadStatus('error');
setUploadMessage(`❌ Upload failed: ${error.response?.data?.detail || error.message}`);
setTimeout(() => setUploadStatus('idle'), 5000);
},
});
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setSelectedFiles(Array.from(e.target.files));
}
};
const handleUpload = async () => {
if (selectedFiles.length === 0) return;
setUploadStatus('uploading');
for (const file of selectedFiles) {
await uploadMutation.mutateAsync(file);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
if (e.dataTransfer.files) {
setSelectedFiles(Array.from(e.dataTransfer.files));
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
return (
<div className="space-y-6">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="card"
>
<div className="flex items-center gap-4">
<div className="p-3 bg-electric-teal/20 rounded-lg">
<FileText className="w-8 h-8 text-electric-teal" />
</div>
<div>
<div className="text-2xl font-bold text-white">
{statsLoading ? '...' : stats?.total_documents || 0}
</div>
<div className="text-sm text-light-grey">Documents</div>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="card"
>
<div className="flex items-center gap-4">
<div className="p-3 bg-cool-blue/20 rounded-lg">
<Database className="w-8 h-8 text-cool-blue" />
</div>
<div>
<div className="text-2xl font-bold text-white">
{statsLoading ? '...' : stats?.total_chunks || 0}
</div>
<div className="text-sm text-light-grey">Chunks</div>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="card"
>
<div className="flex items-center gap-4">
<div className="p-3 bg-blaze-orange/20 rounded-lg">
<Clock className="w-8 h-8 text-blaze-orange" />
</div>
<div>
<div className="text-sm font-semibold text-white">Last Updated</div>
<div className="text-xs text-light-grey">
{statsLoading
? '...'
: stats?.last_updated
? new Date(stats.last_updated).toLocaleDateString()
: 'Never'}
</div>
</div>
</div>
</motion.div>
</div>
{/* Document Upload Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="card"
>
<h2 className="text-xl font-bold text-electric-teal mb-4 flex items-center gap-2">
<Upload className="w-5 h-5" />
Upload Documents
</h2>
<div
onDrop={handleDrop}
onDragOver={handleDragOver}
onClick={() => fileInputRef.current?.click()}
className="border-2 border-dashed border-electric-teal/30 rounded-lg p-12 text-center hover:border-electric-teal/60 transition-colors cursor-pointer"
>
<input
ref={fileInputRef}
type="file"
multiple
onChange={handleFileSelect}
className="hidden"
accept=".txt,.md,.pdf,.py,.js,.ts,.tsx,.jsx,.json,.yaml,.yml,.html,.css,.cpp,.c,.h,.java,.go,.rs,.rb,.php,.sh,.sql"
/>
{selectedFiles.length === 0 ? (
<>
<FileText className="w-12 h-12 text-electric-teal/50 mx-auto mb-4" />
<p className="text-light-grey mb-2">Drag & drop files here, or click to browse</p>
<p className="text-sm text-light-grey/60">Supports: .txt, .md, .pdf, .py, .js, .ts, and more</p>
</>
) : (
<>
<CheckCircle className="w-12 h-12 text-electric-teal mx-auto mb-4" />
<p className="text-white mb-2">{selectedFiles.length} file(s) selected</p>
<div className="text-sm text-light-grey space-y-1">
{selectedFiles.map((file, idx) => (
<div key={idx}>{file.name}</div>
))}
</div>
</>
)}
</div>
{selectedFiles.length > 0 && (
<div className="mt-4 flex gap-2">
<button
onClick={handleUpload}
disabled={uploadStatus === 'uploading'}
className="btn-primary flex-1"
>
{uploadStatus === 'uploading' ? 'Uploading...' : 'Upload Files'}
</button>
<button
onClick={() => setSelectedFiles([])}
className="btn-secondary"
>
Clear
</button>
</div>
)}
{uploadStatus !== 'idle' && (
<div className={`mt-4 p-3 rounded-lg ${
uploadStatus === 'success' ? 'bg-electric-teal/20 border border-electric-teal/50' :
uploadStatus === 'error' ? 'bg-blaze-orange/20 border border-blaze-orange/50' :
'bg-cool-blue/20 border border-cool-blue/50'
}`}>
<p className="text-sm">{uploadMessage || 'Processing...'}</p>
</div>
)}
</motion.div>
{/* Documents List */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="card"
>
<h2 className="text-xl font-bold text-electric-teal mb-4 flex items-center gap-2">
<Database className="w-5 h-5" />
Documents ({documents.length})
</h2>
{docsLoading ? (
<div className="text-center py-8 text-light-grey">Loading documents...</div>
) : documents.length === 0 ? (
<div className="text-center py-8 text-light-grey">
No documents yet. Upload some to get started!
</div>
) : (
<div className="space-y-2 max-h-96 overflow-y-auto">
{documents.map((doc: any, idx: number) => (
<div
key={idx}
className="p-4 bg-cool-blue/5 border border-electric-teal/20 rounded-lg hover:border-electric-teal/50 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="font-semibold text-white mb-1">
{doc.metadata?.source || `Document ${idx + 1}`}
</div>
<div className="text-sm text-light-grey line-clamp-2">
{doc.content?.substring(0, 150)}...
</div>
</div>
<FileText className="w-5 h-5 text-electric-teal flex-shrink-0 ml-4" />
</div>
</div>
))}
</div>
)}
</motion.div>
</div>
);
}

97
frontend/src/index.css Normal file
View file

@ -0,0 +1,97 @@
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800;900&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply bg-deep-night text-white font-mono;
margin: 0;
min-height: 100vh;
}
}
@layer components {
.gradient-text {
background: linear-gradient(135deg, #00FFFF, #4D94FF, #FF8C00);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.glow-teal {
box-shadow: 0 0 20px rgba(0, 255, 255, 0.5);
}
.glow-teal-strong {
box-shadow: 0 0 40px rgba(0, 255, 255, 0.8);
}
.glow-orange {
box-shadow: 0 0 20px rgba(255, 140, 0, 0.5);
}
.glow-orange-strong {
box-shadow: 0 0 40px rgba(255, 140, 0, 0.8);
}
.card {
@apply bg-gradient-to-br from-deep-night to-cool-blue/10 rounded-2xl border border-electric-teal/30 p-6 transition-all duration-300;
}
.card:hover {
@apply border-electric-teal glow-teal transform -translate-y-1;
}
.btn {
@apply px-6 py-3 rounded-lg font-semibold transition-all duration-300 transform hover:scale-105;
}
.btn-primary {
@apply btn bg-gradient-to-r from-electric-teal to-cool-blue text-deep-night glow-teal;
}
.btn-primary:hover {
@apply glow-teal-strong;
}
.btn-secondary {
@apply btn bg-gradient-to-r from-blaze-orange to-blaze-orange/80 text-white glow-orange;
}
.btn-secondary:hover {
@apply glow-orange-strong;
}
.input {
@apply bg-deep-night border border-electric-teal/30 rounded-lg px-4 py-2 text-white placeholder-light-grey focus:outline-none focus:border-electric-teal focus:glow-teal transition-all duration-300;
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #0A0E1A;
}
::-webkit-scrollbar-thumb {
background: #00FFFF;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #4D94FF;
}
}
@layer utilities {
.text-shadow-glow {
text-shadow: 0 0 30px rgba(0, 255, 255, 0.5);
}
.text-shadow-glow-orange {
text-shadow: 0 0 30px rgba(255, 140, 0, 0.5);
}
}

10
frontend/src/main.tsx Normal file
View file

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View file

@ -0,0 +1,285 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Home, Settings, Database, MessageSquare, Plus, Trash2, HelpCircle } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion, AnimatePresence } from 'framer-motion';
import { listKnowledgeBases, createKnowledgeBase, deleteKnowledgeBase, type KnowledgeBase } from '../lib/api';
import ChatInterface from '../components/ChatInterface';
import KBManagement from '../components/KBManagement';
import HelpDocs from '../components/HelpDocs';
export default function MainApp() {
const [selectedKB, setSelectedKB] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'kb' | 'chat' | 'help'>('kb');
const [showCreateKB, setShowCreateKB] = useState(false);
const [newKBName, setNewKBName] = useState('');
const [newKBType, setNewKBType] = useState('generic');
const [newKBDesc, setNewKBDesc] = useState('');
// RAG settings
const [useHybrid, setUseHybrid] = useState(false);
const [useRerank, setUseRerank] = useState(false);
const [useAgentic, setUseAgentic] = useState(false);
const queryClient = useQueryClient();
// Fetch knowledge bases
const { data: kbs = [], isLoading } = useQuery({
queryKey: ['knowledge-bases'],
queryFn: listKnowledgeBases,
});
// Create KB mutation
const createKBMutation = useMutation({
mutationFn: () => createKnowledgeBase(newKBName, newKBType, newKBDesc),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['knowledge-bases'] });
setShowCreateKB(false);
setNewKBName('');
setNewKBType('generic');
setNewKBDesc('');
},
});
// Delete KB mutation
const deleteKBMutation = useMutation({
mutationFn: (kb_name: string) => deleteKnowledgeBase(kb_name),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['knowledge-bases'] });
},
});
const handleCreateKB = () => {
if (newKBName.trim()) {
createKBMutation.mutate();
}
};
return (
<div className="min-h-screen flex flex-col">
{/* Top Navigation */}
<nav className="bg-deep-night border-b border-electric-teal/30 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<img src="/assets/logo.png" alt="Code-Forge" className="h-10" />
<h1 className="text-2xl font-bold gradient-text">CODE-FORGE</h1>
</div>
<div className="flex gap-2">
<Link to="/">
<button className="btn-secondary flex items-center gap-2">
<Home className="w-4 h-4" />
Welcome
</button>
</Link>
</div>
</div>
</nav>
<div className="flex flex-1 overflow-hidden">
{/* Left Sidebar */}
<aside className="w-80 bg-gradient-to-b from-deep-night to-cool-blue/5 border-r border-electric-teal/30 p-6 overflow-y-auto">
{/* Settings Section */}
<div className="mb-6">
<h2 className="text-lg font-bold text-electric-teal mb-3 flex items-center gap-2">
<Settings className="w-5 h-5" />
RAG Settings
</h2>
<div className="space-y-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={useHybrid}
onChange={(e) => setUseHybrid(e.target.checked)}
className="w-4 h-4 accent-electric-teal"
/>
<span className="text-sm">Hybrid Search (Vector + BM25)</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={useRerank}
onChange={(e) => setUseRerank(e.target.checked)}
className="w-4 h-4 accent-electric-teal"
/>
<span className="text-sm">Reranking</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={useAgentic}
onChange={(e) => setUseAgentic(e.target.checked)}
className="w-4 h-4 accent-electric-teal"
/>
<span className="text-sm">Agentic RAG</span>
</label>
</div>
</div>
{/* Knowledge Bases List */}
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-bold text-electric-teal flex items-center gap-2">
<Database className="w-5 h-5" />
Knowledge Bases
</h2>
<button
onClick={() => setShowCreateKB(!showCreateKB)}
className="p-1 rounded hover:bg-electric-teal/20 transition-colors"
>
<Plus className="w-5 h-5 text-electric-teal" />
</button>
</div>
{/* Create KB Form */}
<AnimatePresence>
{showCreateKB && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mb-4 p-4 bg-cool-blue/10 rounded-lg border border-electric-teal/30"
>
<input
type="text"
placeholder="KB Name"
value={newKBName}
onChange={(e) => setNewKBName(e.target.value)}
className="input w-full mb-2"
/>
<select
value={newKBType}
onChange={(e) => setNewKBType(e.target.value)}
className="input w-full mb-2"
>
<option value="generic">Generic</option>
<option value="code">Code</option>
<option value="documentation">Documentation</option>
<option value="agent-os">Agent OS</option>
</select>
<input
type="text"
placeholder="Description (optional)"
value={newKBDesc}
onChange={(e) => setNewKBDesc(e.target.value)}
className="input w-full mb-2"
/>
<button
onClick={handleCreateKB}
disabled={!newKBName.trim() || createKBMutation.isPending}
className="btn-primary w-full text-sm"
>
{createKBMutation.isPending ? 'Creating...' : 'Create KB'}
</button>
</motion.div>
)}
</AnimatePresence>
{/* KB List */}
<div className="space-y-2">
{isLoading ? (
<div className="text-light-grey text-sm">Loading...</div>
) : kbs.length === 0 ? (
<div className="text-light-grey text-sm">No knowledge bases yet</div>
) : (
kbs.map((kb: KnowledgeBase) => (
<div
key={kb.name}
className={`p-3 rounded-lg border transition-all cursor-pointer ${
selectedKB === kb.name
? 'bg-electric-teal/20 border-electric-teal'
: 'bg-cool-blue/5 border-electric-teal/20 hover:border-electric-teal/50'
}`}
onClick={() => setSelectedKB(kb.name)}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="font-semibold text-white">{kb.name}</div>
<div className="text-xs text-light-grey">{kb.kb_type}</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
if (confirm(`Delete "${kb.name}"?`)) {
deleteKBMutation.mutate(kb.name);
}
}}
className="p-1 rounded hover:bg-blaze-orange/20 transition-colors"
>
<Trash2 className="w-4 h-4 text-blaze-orange" />
</button>
</div>
</div>
))
)}
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col overflow-hidden">
{/* Tabs */}
<div className="bg-deep-night border-b border-electric-teal/30 px-6 py-3 flex gap-4">
<button
onClick={() => setActiveTab('kb')}
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
activeTab === 'kb'
? 'bg-electric-teal text-deep-night'
: 'text-light-grey hover:text-white'
}`}
>
<Database className="w-4 h-4 inline mr-2" />
KB Management
</button>
<button
onClick={() => setActiveTab('chat')}
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
activeTab === 'chat'
? 'bg-electric-teal text-deep-night'
: 'text-light-grey hover:text-white'
}`}
disabled={!selectedKB}
>
<MessageSquare className="w-4 h-4 inline mr-2" />
Chat
</button>
<button
onClick={() => setActiveTab('help')}
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
activeTab === 'help'
? 'bg-electric-teal text-deep-night'
: 'text-light-grey hover:text-white'
}`}
>
<HelpCircle className="w-4 h-4 inline mr-2" />
Help
</button>
</div>
{/* Tab Content */}
<div className="flex-1 overflow-auto p-6">
{activeTab === 'help' ? (
<HelpDocs />
) : !selectedKB ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Database className="w-16 h-16 text-electric-teal/50 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-light-grey mb-2">No Knowledge Base Selected</h2>
<p className="text-light-grey">Create or select a knowledge base to get started</p>
</div>
</div>
) : activeTab === 'kb' ? (
<KBManagement kbName={selectedKB} />
) : (
<ChatInterface
kbName={selectedKB}
useHybrid={useHybrid}
useRerank={useRerank}
useAgentic={useAgentic}
/>
)}
</div>
</main>
</div>
</div>
);
}

View file

@ -0,0 +1,155 @@
import { motion } from 'framer-motion';
import { Database, Cpu, Zap, BookOpen, Brain, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function Welcome() {
return (
<div className="min-h-screen overflow-hidden">
{/* Animated background */}
<div className="fixed inset-0 -z-10">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-electric-teal/20 rounded-full blur-3xl animate-pulse-glow" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-blaze-orange/20 rounded-full blur-3xl animate-pulse-glow" style={{ animationDelay: '2s' }} />
<div className="absolute top-1/2 left-1/2 w-96 h-96 bg-cool-blue/10 rounded-full blur-3xl animate-pulse-glow" style={{ animationDelay: '4s' }} />
</div>
<div className="container mx-auto px-6 py-12">
{/* Hero Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
{/* Hero Image */}
<motion.div
className="mb-8"
animate={{ y: [0, -20, 0] }}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
>
<img
src="/assets/hero.png"
alt="Code-Forge Hero"
className="mx-auto max-w-2xl w-full drop-shadow-[0_0_50px_rgba(0,255,255,0.6)]"
/>
</motion.div>
{/* Subtitle */}
<p className="text-2xl text-electric-teal mb-8">
Production-Grade RAG System with PostgreSQL + pgvector
</p>
{/* CTA Button */}
<Link to="/app">
<motion.button
className="btn-primary text-xl px-12 py-4 inline-flex items-center gap-3 font-bold"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Get Started
<ArrowRight className="w-6 h-6" />
</motion.button>
</Link>
</motion.div>
{/* Features Grid */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3, duration: 0.8 }}
className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16"
>
<FeatureCard
icon={<Database className="w-8 h-8" />}
title="PostgreSQL + pgvector"
description="ACID-compliant vector database with production-grade reliability"
delay={0.1}
/>
<FeatureCard
icon={<Cpu className="w-8 h-8" />}
title="Local LLMs"
description="Powered by Ollama - no API keys, complete privacy"
delay={0.2}
/>
<FeatureCard
icon={<Zap className="w-8 h-8" />}
title="MCP Integration"
description="Expose knowledge bases to AI agents via Model Context Protocol"
delay={0.3}
/>
<FeatureCard
icon={<BookOpen className="w-8 h-8" />}
title="Multi-KB Architecture"
description="Isolated knowledge bases per project with type-aware organization"
delay={0.4}
/>
<FeatureCard
icon={<Brain className="w-8 h-8" />}
title="Advanced RAG"
description="Vector search, hybrid search, reranking, and agentic RAG modes"
delay={0.5}
/>
<FeatureCard
icon={<Zap className="w-8 h-8" />}
title="High-Tech UI"
description="Electric teal & blaze orange design that looks fucking amazing"
delay={0.6}
/>
</motion.div>
{/* Quick Start */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6, duration: 0.8 }}
className="max-w-3xl mx-auto"
>
<div className="card">
<h2 className="text-3xl font-bold mb-6 gradient-text">🎯 Quick Start</h2>
<ol className="space-y-4 text-lg">
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-8 h-8 rounded-full bg-electric-teal text-deep-night flex items-center justify-center font-bold">1</span>
<span>Click <strong className="text-electric-teal">"Get Started"</strong> to access the main application</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-8 h-8 rounded-full bg-electric-teal text-deep-night flex items-center justify-center font-bold">2</span>
<span>Create a <strong className="text-cool-blue">Knowledge Base</strong> from the left panel</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-8 h-8 rounded-full bg-electric-teal text-deep-night flex items-center justify-center font-bold">3</span>
<span>Upload documents or import a directory</span>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-8 h-8 rounded-full bg-electric-teal text-deep-night flex items-center justify-center font-bold">4</span>
<span>Start chatting with your knowledge base using <strong className="text-blaze-orange">advanced RAG</strong></span>
</li>
</ol>
</div>
</motion.div>
</div>
</div>
);
}
interface FeatureCardProps {
icon: React.ReactNode;
title: string;
description: string;
delay: number;
}
function FeatureCard({ icon, title, description, delay }: FeatureCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay, duration: 0.5 }}
whileHover={{ scale: 1.02 }}
className="card"
>
<div className="text-electric-teal mb-4">{icon}</div>
<h3 className="text-xl font-bold mb-2 text-white">{title}</h3>
<p className="text-light-grey">{description}</p>
</motion.div>
);
}

View file

@ -0,0 +1,49 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
plugins: [
require('@tailwindcss/typography'),
],
theme: {
extend: {
colors: {
// Code-Forge Design Guide colors
'deep-night': '#0A0E1A',
'electric-teal': '#00FFFF',
'blaze-orange': '#FF8C00',
'cool-blue': '#4D94FF',
'light-grey': '#C0C0C0',
},
fontFamily: {
mono: ['"JetBrains Mono"', 'monospace'],
},
animation: {
'float': 'float 3s ease-in-out infinite',
'pulse-glow': 'pulse-glow 8s ease-in-out infinite',
'gradient': 'gradient 8s linear infinite',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-20px)' },
},
'pulse-glow': {
'0%, 100%': { transform: 'scale(1)', opacity: '0.5' },
'50%': { transform: 'scale(1.1)', opacity: '0.8' },
},
gradient: {
'0%, 100%': { backgroundPosition: '0% 50%' },
'50%': { backgroundPosition: '100% 50%' },
},
},
backgroundSize: {
'200': '200% 200%',
},
},
},
plugins: [],
}

View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
frontend/tsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
frontend/vite.config.ts Normal file
View file

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

View file

@ -6,20 +6,27 @@ Exposes RAG functionality via Model Context Protocol with HTTP transport.
import logging
import sys
from pathlib import Path
from typing import List
from typing import Any, Dict, List
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from fastapi import FastAPI
from fastapi import FastAPI, Request, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from mcp.server.fastmcp import FastMCP
from fastapi.responses import JSONResponse
from pydantic import BaseModel
import uvicorn
import requests
import psycopg2
from typing import Optional
from app.core.chroma_manager import get_chroma_manager
from app.core.pg_manager import get_pg_manager
from app.core.config import Config
from app.core.kb_metadata import get_collection_stats, get_documents_metadata
from app.core.kb_types import KBType
from app.core.rag_engine import RAGEngine
from app.core.agent_os_ingestion import AgentOSIngestion
from app.core.agent_os_parser import AgentOSContentType
# Configure logging
logging.basicConfig(
@ -28,11 +35,10 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
# Initialize FastAPI and FastMCP
# Initialize FastAPI
app = FastAPI(title="Code-Forge MCP Server")
mcp = FastMCP("code-forge")
# Add CORS middleware for Claude Desktop
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
@ -41,82 +47,175 @@ app.add_middleware(
allow_headers=["*"],
)
# MCP Tools Registry
TOOLS = {
"search_knowledge_base": {
"description": "Search a knowledge base using RAG with optional advanced features",
"inputSchema": {
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base to search"},
"query": {"type": "string", "description": "The search query"},
"use_hybrid": {"type": "boolean", "description": "Enable hybrid search (vector + BM25)", "default": False},
"use_rerank": {"type": "boolean", "description": "Enable reranking for better relevance", "default": False},
"use_agentic": {"type": "boolean", "description": "Enable agentic RAG for complex queries", "default": False}
},
"required": ["kb_name", "query"]
}
},
"list_knowledge_bases": {
"description": "List all available knowledge bases with type metadata",
"inputSchema": {
"type": "object",
"properties": {}
}
},
"list_knowledge_bases_by_type": {
"description": "List knowledge bases filtered by type (e.g., agent-os, code, documentation)",
"inputSchema": {
"type": "object",
"properties": {
"kb_type": {
"type": "string",
"description": "KB type to filter by",
"enum": ["generic", "code", "documentation", "agent-os"]
}
},
"required": ["kb_type"]
}
},
"create_knowledge_base": {
"description": "Create a new knowledge base with specified type",
"inputSchema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name for the new knowledge base"},
"kb_type": {
"type": "string",
"description": "Type of knowledge base",
"enum": ["generic", "code", "documentation", "agent-os"],
"default": "generic"
},
"description": {"type": "string", "description": "Optional description", "default": ""}
},
"required": ["name"]
}
},
"get_kb_stats": {
"description": "Get statistics for a knowledge base",
"inputSchema": {
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"}
},
"required": ["kb_name"]
}
},
"list_documents": {
"description": "List all documents in a knowledge base",
"inputSchema": {
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the knowledge base"}
},
"required": ["kb_name"]
}
},
"ingest_agent_os_profile": {
"description": "Ingest an Agent OS profile directory into a knowledge base",
"inputSchema": {
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the Agent OS knowledge base"},
"profile_path": {"type": "string", "description": "Path to the Agent OS profile directory"}
},
"required": ["kb_name", "profile_path"]
}
},
"get_agent_os_stats": {
"description": "Get statistics about Agent OS content in a knowledge base",
"inputSchema": {
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the Agent OS knowledge base"}
},
"required": ["kb_name"]
}
},
"get_standards": {
"description": "Get coding standards from an Agent OS knowledge base",
"inputSchema": {
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the Agent OS knowledge base"},
"query": {"type": "string", "description": "Optional search query to filter standards", "default": ""}
},
"required": ["kb_name"]
}
},
"get_workflows": {
"description": "Get development workflows from an Agent OS knowledge base",
"inputSchema": {
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the Agent OS knowledge base"},
"query": {"type": "string", "description": "Optional search query to filter workflows", "default": ""}
},
"required": ["kb_name"]
}
},
"get_specs": {
"description": "Get feature specifications from an Agent OS knowledge base",
"inputSchema": {
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the Agent OS knowledge base"},
"query": {"type": "string", "description": "Optional search query to filter specs", "default": ""}
},
"required": ["kb_name"]
}
},
"get_product_context": {
"description": "Get product vision and context from an Agent OS knowledge base",
"inputSchema": {
"type": "object",
"properties": {
"kb_name": {"type": "string", "description": "Name of the Agent OS knowledge base"}
},
"required": ["kb_name"]
}
}
}
@mcp.tool()
async def search_knowledge_base(
kb_name: str,
query: str,
use_hybrid: bool = False,
use_rerank: bool = False,
use_agentic: bool = False
) -> dict:
"""
Search a knowledge base using RAG with optional advanced features.
Args:
kb_name: Name of the knowledge base to search
query: The search query
use_hybrid: Enable hybrid search (vector + BM25 keyword search)
use_rerank: Enable reranking of results for better relevance
use_agentic: Enable agentic RAG for complex multi-step queries
Returns:
dict with 'answer' and 'sources' keys
"""
# Tool implementation functions
async def search_knowledge_base(kb_name: str, query: str, use_hybrid: bool = False,
use_rerank: bool = False, use_agentic: bool = False) -> dict:
"""Search a knowledge base using RAG."""
try:
# Validate KB exists
chroma_manager = get_chroma_manager()
if not chroma_manager.collection_exists(kb_name):
return {
"error": f"Knowledge base '{kb_name}' not found",
"answer": "",
"sources": []
}
# Check if KB is empty
count = chroma_manager.get_collection_count(kb_name)
pg_manager = get_pg_manager()
if not pg_manager.collection_exists(kb_name):
return {"error": f"Knowledge base '{kb_name}' not found", "answer": "", "sources": []}
count = pg_manager.get_collection_count(kb_name)
if count == 0:
return {
"error": f"Knowledge base '{kb_name}' is empty. Please upload documents first.",
"answer": "",
"sources": []
}
# Initialize RAG engine
return {"error": f"Knowledge base '{kb_name}' is empty", "answer": "", "sources": []}
rag_engine = RAGEngine(kb_name)
# Execute query
result = rag_engine.query(
question=query,
use_hybrid=use_hybrid,
use_rerank=use_rerank,
use_agentic=use_agentic
)
result = rag_engine.query(question=query, use_hybrid=use_hybrid,
use_rerank=use_rerank, use_agentic=use_agentic)
logger.info(f"Query executed on {kb_name}: {query[:50]}...")
return result
except Exception as e:
logger.error(f"Search failed: {e}")
return {
"error": str(e),
"answer": "",
"sources": []
}
return {"error": str(e), "answer": "", "sources": []}
@mcp.tool()
async def list_knowledge_bases() -> List[str]:
"""
List all available knowledge bases.
Returns:
List of knowledge base names
"""
async def list_knowledge_bases() -> List[Dict[str, any]]:
"""List all available knowledge bases with metadata."""
try:
chroma_manager = get_chroma_manager()
kbs = chroma_manager.list_collections()
pg_manager = get_pg_manager()
kbs = pg_manager.list_collections()
logger.info(f"Listed {len(kbs)} knowledge bases")
return kbs
except Exception as e:
@ -124,42 +223,19 @@ async def list_knowledge_bases() -> List[str]:
return []
@mcp.tool()
async def get_kb_stats(kb_name: str) -> dict:
"""
Get statistics for a knowledge base.
Args:
kb_name: Name of the knowledge base
Returns:
dict with total_documents, total_chunks, and last_updated
"""
"""Get statistics for a knowledge base."""
try:
stats = get_collection_stats(kb_name)
logger.info(f"Retrieved stats for {kb_name}")
return stats
except Exception as e:
logger.error(f"Failed to get stats for {kb_name}: {e}")
return {
"error": str(e),
"total_documents": 0,
"total_chunks": 0,
"last_updated": None
}
return {"error": str(e), "total_documents": 0, "total_chunks": 0, "last_updated": None}
@mcp.tool()
async def list_documents(kb_name: str) -> List[dict]:
"""
List all documents in a knowledge base.
Args:
kb_name: Name of the knowledge base
Returns:
List of document metadata dicts
"""
"""List all documents in a knowledge base."""
try:
docs = get_documents_metadata(kb_name)
logger.info(f"Listed {len(docs)} documents in {kb_name}")
@ -169,40 +245,605 @@ async def list_documents(kb_name: str) -> List[dict]:
return []
# Health check endpoint
@app.get("/health")
async def health_check():
"""Health check endpoint."""
return {"status": "healthy", "service": "code-forge-mcp"}
# MCP endpoint
@app.post("/mcp")
async def mcp_endpoint(request: dict):
"""
MCP protocol endpoint.
Handles JSON-RPC style requests from MCP clients.
"""
async def list_knowledge_bases_by_type(kb_type: str) -> List[Dict[str, any]]:
"""List knowledge bases filtered by type."""
try:
# FastMCP handles the request routing
return await mcp.handle_request(request)
pg_manager = get_pg_manager()
kb_type_enum = KBType(kb_type)
kbs = pg_manager.list_collections_by_type(kb_type_enum)
logger.info(f"Listed {len(kbs)} knowledge bases of type {kb_type}")
return kbs
except Exception as e:
logger.error(f"MCP request failed: {e}")
logger.error(f"Failed to list knowledge bases by type {kb_type}: {e}")
return []
async def create_knowledge_base(name: str, kb_type: str = "generic", description: str = "") -> dict:
"""Create a new knowledge base."""
try:
pg_manager = get_pg_manager()
# Check if already exists
if pg_manager.collection_exists(name):
return {
"success": False,
"error": f"Knowledge base '{name}' already exists"
}
# Create collection
kb_type_enum = KBType(kb_type)
success = pg_manager.create_collection(
name=name,
kb_type=kb_type_enum,
description=description
)
if success:
logger.info(f"Created knowledge base: {name} (type: {kb_type})")
return {
"success": True,
"name": name,
"kb_type": kb_type,
"description": description
}
else:
return {
"success": False,
"error": "Failed to create knowledge base"
}
except Exception as e:
logger.error(f"Failed to create knowledge base {name}: {e}")
return {
"success": False,
"error": str(e)
}
async def ingest_agent_os_profile(kb_name: str, profile_path: str) -> dict:
"""Ingest an Agent OS profile into a knowledge base."""
try:
pg_manager = get_pg_manager()
agent_os_ingestion = AgentOSIngestion(pg_manager)
stats = agent_os_ingestion.ingest_profile(
kb_name=kb_name,
profile_path=profile_path
)
logger.info(f"Ingested Agent OS profile from {profile_path} into {kb_name}")
return stats
except Exception as e:
logger.error(f"Failed to ingest Agent OS profile: {e}")
return {
"success": False,
"error": str(e),
"documents_processed": 0
}
async def get_agent_os_stats(kb_name: str) -> dict:
"""Get statistics about Agent OS content in a knowledge base."""
try:
pg_manager = get_pg_manager()
agent_os_ingestion = AgentOSIngestion(pg_manager)
stats = agent_os_ingestion.get_profile_stats(kb_name)
logger.info(f"Retrieved Agent OS stats for {kb_name}")
return stats
except Exception as e:
logger.error(f"Failed to get Agent OS stats for {kb_name}: {e}")
return {
"total_documents": 0,
"documents_by_type": {},
"error": str(e)
}
async def get_standards(kb_name: str, query: str = "") -> List[dict]:
"""Get coding standards from an Agent OS knowledge base."""
try:
pg_manager = get_pg_manager()
agent_os_ingestion = AgentOSIngestion(pg_manager)
results = agent_os_ingestion.search_by_type(
kb_name=kb_name,
content_type=AgentOSContentType.STANDARD,
query=query if query else None,
limit=20
)
logger.info(f"Retrieved {len(results)} standards from {kb_name}")
return results
except Exception as e:
logger.error(f"Failed to get standards from {kb_name}: {e}")
return []
async def get_workflows(kb_name: str, query: str = "") -> List[dict]:
"""Get development workflows from an Agent OS knowledge base."""
try:
pg_manager = get_pg_manager()
agent_os_ingestion = AgentOSIngestion(pg_manager)
results = agent_os_ingestion.search_by_type(
kb_name=kb_name,
content_type=AgentOSContentType.WORKFLOW,
query=query if query else None,
limit=20
)
logger.info(f"Retrieved {len(results)} workflows from {kb_name}")
return results
except Exception as e:
logger.error(f"Failed to get workflows from {kb_name}: {e}")
return []
async def get_specs(kb_name: str, query: str = "") -> List[dict]:
"""Get feature specifications from an Agent OS knowledge base."""
try:
pg_manager = get_pg_manager()
agent_os_ingestion = AgentOSIngestion(pg_manager)
results = agent_os_ingestion.search_by_type(
kb_name=kb_name,
content_type=AgentOSContentType.SPEC,
query=query if query else None,
limit=20
)
logger.info(f"Retrieved {len(results)} specs from {kb_name}")
return results
except Exception as e:
logger.error(f"Failed to get specs from {kb_name}: {e}")
return []
async def get_product_context(kb_name: str) -> List[dict]:
"""Get product vision and context from an Agent OS knowledge base."""
try:
pg_manager = get_pg_manager()
agent_os_ingestion = AgentOSIngestion(pg_manager)
results = agent_os_ingestion.search_by_type(
kb_name=kb_name,
content_type=AgentOSContentType.PRODUCT,
query=None,
limit=20
)
logger.info(f"Retrieved {len(results)} product context documents from {kb_name}")
return results
except Exception as e:
logger.error(f"Failed to get product context from {kb_name}: {e}")
return []
# Pydantic models for REST API
class CreateKBRequest(BaseModel):
name: str
kb_type: str = "generic"
description: str = ""
class ChatRequest(BaseModel):
query: str
use_hybrid: bool = False
use_rerank: bool = False
use_agentic: bool = False
# REST API Endpoints for UI
@app.get("/api/kb")
async def api_list_knowledge_bases():
"""REST API: List all knowledge bases."""
kbs = await list_knowledge_bases()
return {"knowledge_bases": kbs}
@app.post("/api/kb")
async def api_create_knowledge_base(request: CreateKBRequest):
"""REST API: Create a new knowledge base."""
result = await create_knowledge_base(
name=request.name,
kb_type=request.kb_type,
description=request.description
)
if not result.get("success"):
raise HTTPException(status_code=400, detail=result.get("error"))
return result
@app.delete("/api/kb/{kb_name}")
async def api_delete_knowledge_base(kb_name: str):
"""REST API: Delete a knowledge base."""
try:
pg_manager = get_pg_manager()
if not pg_manager.collection_exists(kb_name):
raise HTTPException(status_code=404, detail=f"Knowledge base '{kb_name}' not found")
success = pg_manager.delete_collection(kb_name)
if success:
return {"success": True, "message": f"Knowledge base '{kb_name}' deleted"}
else:
raise HTTPException(status_code=500, detail="Failed to delete knowledge base")
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete KB {kb_name}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/kb/{kb_name}/stats")
async def api_get_kb_stats(kb_name: str):
"""REST API: Get knowledge base statistics."""
stats = await get_kb_stats(kb_name)
if "error" in stats:
raise HTTPException(status_code=404, detail=stats["error"])
return stats
@app.get("/api/kb/{kb_name}/documents")
async def api_list_documents(kb_name: str):
"""REST API: List documents in a knowledge base."""
docs = await list_documents(kb_name)
return {"documents": docs}
@app.post("/api/kb/{kb_name}/chat")
async def api_chat(kb_name: str, request: ChatRequest):
"""REST API: Chat with a knowledge base."""
result = await search_knowledge_base(
kb_name=kb_name,
query=request.query,
use_hybrid=request.use_hybrid,
use_rerank=request.use_rerank,
use_agentic=request.use_agentic
)
if "error" in result and result["error"]:
raise HTTPException(status_code=400, detail=result["error"])
return result
@app.post("/api/kb/{kb_name}/upload")
async def api_upload_document(kb_name: str, file: UploadFile = File(...)):
"""REST API: Upload a single document to a knowledge base."""
import tempfile
import os
from app.core.ingestion import ingest_file
# Validate KB exists
pg_manager = get_pg_manager()
kb = pg_manager.get_collection(kb_name)
if not kb:
raise HTTPException(status_code=404, detail=f"Knowledge base '{kb_name}' not found")
# Save uploaded file to temp location
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=Path(file.filename).suffix) as tmp_file:
content = await file.read()
tmp_file.write(content)
tmp_path = tmp_file.name
# Ingest the file
result = ingest_file(tmp_path, kb_name, file.filename)
# Clean up temp file
os.unlink(tmp_path)
if result["status"] == "error":
raise HTTPException(status_code=400, detail=result.get("error", "Upload failed"))
return {
"success": True,
"filename": result["filename"],
"chunks": result["chunks"],
"file_type": result["file_type"]
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Upload failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/kb/{kb_name}/import")
async def api_import_directory(kb_name: str, directory_path: str):
"""REST API: Import all files from a directory into a knowledge base."""
from app.core.ingestion import ingest_directory
# Validate KB exists
pg_manager = get_pg_manager()
kb = pg_manager.get_collection(kb_name)
if not kb:
raise HTTPException(status_code=404, detail=f"Knowledge base '{kb_name}' not found")
# Validate directory exists
if not Path(directory_path).exists():
raise HTTPException(status_code=404, detail=f"Directory not found: {directory_path}")
try:
results = ingest_directory(directory_path, kb_name)
# Count successes and failures
successes = [r for r in results if r.get("status") == "success"]
failures = [r for r in results if r.get("status") == "error"]
return {
"success": True,
"total_files": len(results),
"successful": len(successes),
"failed": len(failures),
"results": results
}
except Exception as e:
logger.error(f"Directory import failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/ollama/models")
async def api_list_ollama_models():
"""REST API: List available Ollama models."""
try:
response = requests.get(f"{Config.OLLAMA_HOST}/api/tags", timeout=2)
if response.status_code == 200:
models = response.json().get("models", [])
return {"models": models}
else:
raise HTTPException(status_code=503, detail="Ollama service unavailable")
except Exception as e:
logger.error(f"Failed to list Ollama models: {e}")
raise HTTPException(status_code=503, detail=str(e))
# Health check endpoint (detailed version at end of file)
# MCP Protocol Endpoints
@app.post("/")
async def mcp_root(request: Request):
"""Main MCP endpoint - handles JSON-RPC requests."""
return await handle_mcp_request(request)
@app.post("/mcp")
async def mcp_endpoint(request: Request):
"""Alternative MCP endpoint path."""
return await handle_mcp_request(request)
async def handle_mcp_request(request: Request) -> JSONResponse:
"""Handle MCP JSON-RPC requests."""
try:
body = await request.json()
method = body.get("method")
params = body.get("params", {})
request_id = body.get("id")
logger.info(f"MCP request: {method}")
# Handle MCP protocol methods
if method == "initialize":
return JSONResponse({
"jsonrpc": "2.0",
"id": request_id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "code-forge",
"version": "1.0.0"
}
}
})
elif method == "tools/list":
tools_list = [
{"name": name, **info}
for name, info in TOOLS.items()
]
return JSONResponse({
"jsonrpc": "2.0",
"id": request_id,
"result": {"tools": tools_list}
})
elif method == "tools/call":
tool_name = params.get("name")
arguments = params.get("arguments", {})
# Route to appropriate tool function
if tool_name == "search_knowledge_base":
result = await search_knowledge_base(**arguments)
elif tool_name == "list_knowledge_bases":
result = await list_knowledge_bases()
elif tool_name == "list_knowledge_bases_by_type":
result = await list_knowledge_bases_by_type(**arguments)
elif tool_name == "create_knowledge_base":
result = await create_knowledge_base(**arguments)
elif tool_name == "get_kb_stats":
result = await get_kb_stats(**arguments)
elif tool_name == "list_documents":
result = await list_documents(**arguments)
elif tool_name == "ingest_agent_os_profile":
result = await ingest_agent_os_profile(**arguments)
elif tool_name == "get_agent_os_stats":
result = await get_agent_os_stats(**arguments)
elif tool_name == "get_standards":
result = await get_standards(**arguments)
elif tool_name == "get_workflows":
result = await get_workflows(**arguments)
elif tool_name == "get_specs":
result = await get_specs(**arguments)
elif tool_name == "get_product_context":
result = await get_product_context(**arguments)
else:
return JSONResponse({
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": -32601,
"message": f"Unknown tool: {tool_name}"
}
})
return JSONResponse({
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [
{
"type": "text",
"text": str(result)
}
]
}
})
else:
return JSONResponse({
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": -32601,
"message": f"Method not found: {method}"
}
})
except Exception as e:
logger.error(f"MCP request failed: {e}", exc_info=True)
return JSONResponse({
"jsonrpc": "2.0",
"id": body.get("id") if 'body' in locals() else None,
"error": {
"code": -32603,
"message": str(e)
},
"id": request.get("id")
}
})
@app.get("/health")
async def health_check():
"""
Health check endpoint that verifies all system components.
Returns detailed status for monitoring.
"""
health_status = {
"status": "healthy",
"components": {},
"timestamp": None
}
from datetime import datetime
health_status["timestamp"] = datetime.utcnow().isoformat()
# Check PostgreSQL
try:
pg_manager = get_pg_manager()
collections = pg_manager.list_collections()
# Try to get schema version
conn = pg_manager.get_connection()
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public';")
table_count = cursor.fetchone()[0]
cursor.close()
pg_manager.return_connection(conn)
health_status["components"]["postgresql"] = {
"status": "healthy",
"connected": True,
"database": "codeforge",
"tables": table_count,
"knowledge_bases": len(collections)
}
except Exception as e:
health_status["status"] = "unhealthy"
health_status["components"]["postgresql"] = {
"status": "unhealthy",
"connected": False,
"error": str(e)
}
# Check Ollama
try:
response = requests.get(f"{Config.OLLAMA_HOST}/api/tags", timeout=2)
if response.status_code == 200:
models = response.json().get("models", [])
model_names = [m["name"] for m in models]
# Check for required models
required_models = ["nomic-embed-text:latest", "llama3.2:3b"]
missing_models = [m for m in required_models if m not in model_names]
health_status["components"]["ollama"] = {
"status": "healthy" if not missing_models else "degraded",
"running": True,
"models_installed": len(models),
"required_models": required_models,
"missing_models": missing_models
}
if missing_models:
health_status["status"] = "degraded"
else:
health_status["status"] = "degraded"
health_status["components"]["ollama"] = {
"status": "degraded",
"running": True,
"error": f"Unexpected status code: {response.status_code}"
}
except Exception as e:
health_status["status"] = "unhealthy"
health_status["components"]["ollama"] = {
"status": "unhealthy",
"running": False,
"error": str(e)
}
# Check pgvector extension
try:
pg_manager = get_pg_manager()
conn = pg_manager.get_connection()
cursor = conn.cursor()
cursor.execute("SELECT extversion FROM pg_extension WHERE extname='vector';")
result = cursor.fetchone()
cursor.close()
pg_manager.return_connection(conn)
if result:
health_status["components"]["pgvector"] = {
"status": "healthy",
"installed": True,
"version": result[0]
}
else:
health_status["status"] = "unhealthy"
health_status["components"]["pgvector"] = {
"status": "unhealthy",
"installed": False,
"error": "pgvector extension not found"
}
except Exception as e:
health_status["status"] = "unhealthy"
health_status["components"]["pgvector"] = {
"status": "unhealthy",
"installed": False,
"error": str(e)
}
# Return appropriate HTTP status code
status_code = 200 if health_status["status"] in ["healthy", "degraded"] else 503
return JSONResponse(content=health_status, status_code=status_code)
def main():
"""Start the MCP server."""
logger.info(f"Starting Code-Forge MCP Server on {Config.MCP_SERVER_HOST}:{Config.MCP_SERVER_PORT}")
logger.info(f"MCP endpoint: http://{Config.MCP_SERVER_HOST}:{Config.MCP_SERVER_PORT}/mcp")
uvicorn.run(
app,
host=Config.MCP_SERVER_HOST,

View file

@ -5,10 +5,13 @@ streamlit>=1.30.0
llama-index>=0.10.0
llama-index-llms-ollama>=0.1.0
llama-index-embeddings-ollama>=0.1.0
llama-index-vector-stores-chroma>=0.1.0
llama-index-vector-stores-postgres>=0.1.0
# Vector Database
chromadb>=0.4.22
psycopg2-binary>=2.9.9
pgvector>=0.2.0
sqlalchemy>=2.0.0
numpy>=1.24.0
# Document Processing
pymupdf>=1.23.0

330
setup.sh Executable file
View file

@ -0,0 +1,330 @@
#!/bin/bash
# Code-Forge Setup Script
# Automated setup for first-time users
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
NC='\033[0m' # No Color
# Archon-inspired banner
echo -e "${PURPLE}"
cat << "EOF"
╔═══════════════════════════════════════════════════════════════╗
║ ║
║ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
║ ██║ ██║ ██║██║ ██║█████╗ █████╗ ██║ ██║██████╔╝██║ ███╗█████╗
║ ██║ ██║ ██║██║ ██║██╔══╝ ██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝
║ ╚██████╗╚██████╔╝██████╔╝███████╗ ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗
║ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝
║ ║
║ Localized Multi-Knowledge-Base RAG System ║
║ with MCP Integration ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
EOF
echo -e "${NC}"
echo -e "${BLUE}🚀 Starting Code-Forge setup...${NC}\n"
# Function to print status
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[✓]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[!]${NC} $1"
}
print_error() {
echo -e "${RED}[✗]${NC} $1"
}
# Check if running on macOS
if [[ "$OSTYPE" != "darwin"* ]]; then
print_error "This script is designed for macOS. For other platforms, please follow manual setup instructions."
exit 1
fi
print_success "Running on macOS"
# ============================================================================
# 1. Check Docker
# ============================================================================
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${PURPLE} Step 1: Docker${NC}"
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
if command -v docker &> /dev/null; then
DOCKER_VERSION=$(docker --version | cut -d ' ' -f3 | cut -d ',' -f1)
print_success "Docker installed (version $DOCKER_VERSION)"
# Check if Docker is running
if docker info &> /dev/null; then
print_success "Docker daemon is running"
else
print_error "Docker is installed but not running"
print_status "Please start Docker Desktop and run this script again"
exit 1
fi
else
print_error "Docker not found"
print_status "Please install Docker Desktop from: https://www.docker.com/products/docker-desktop"
exit 1
fi
# ============================================================================
# 2. Check PostgreSQL
# ============================================================================
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${PURPLE} Step 2: PostgreSQL${NC}"
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
if command -v psql &> /dev/null; then
PG_VERSION=$(psql --version | cut -d ' ' -f3)
print_success "PostgreSQL installed (version $PG_VERSION)"
else
print_error "PostgreSQL not found"
print_status "Please install PostgreSQL from: https://postgresapp.com/"
print_status "Or via Homebrew: brew install postgresql@17"
exit 1
fi
# Test PostgreSQL connection
print_status "Testing PostgreSQL connection..."
if psql -c "SELECT version();" &> /dev/null; then
print_success "PostgreSQL connection successful"
else
print_error "Cannot connect to PostgreSQL"
print_status "Please ensure PostgreSQL is running"
print_status "If using Postgres.app, make sure it's started"
exit 1
fi
# ============================================================================
# 3. Install pgvector Extension
# ============================================================================
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${PURPLE} Step 3: pgvector Extension${NC}"
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
print_status "Checking for pgvector extension..."
# Check if pgvector is available
if psql -c "CREATE EXTENSION IF NOT EXISTS vector;" &> /dev/null; then
PGVECTOR_VERSION=$(psql -c "SELECT extversion FROM pg_extension WHERE extname='vector';" -t 2>/dev/null | xargs)
if [ -n "$PGVECTOR_VERSION" ]; then
print_success "pgvector extension installed (version $PGVECTOR_VERSION)"
else
print_success "pgvector extension enabled"
fi
else
print_error "pgvector extension not available"
print_status "Installing pgvector..."
# Try to install via Homebrew
if command -v brew &> /dev/null; then
brew install pgvector
print_success "pgvector installed via Homebrew"
else
print_error "Homebrew not found. Please install pgvector manually:"
print_status "Visit: https://github.com/pgvector/pgvector"
exit 1
fi
fi
# ============================================================================
# 4. Create Database
# ============================================================================
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${PURPLE} Step 4: Database Setup${NC}"
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
print_status "Creating 'codeforge' database..."
# Check if database exists
if psql -lqt | cut -d \| -f 1 | grep -qw codeforge; then
print_warning "Database 'codeforge' already exists"
read -p "$(echo -e ${YELLOW}Do you want to recreate it? This will DELETE all existing data! [y/N]: ${NC})" -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
print_status "Dropping existing database..."
psql -c "DROP DATABASE codeforge;" 2>/dev/null || true
psql -c "CREATE DATABASE codeforge;"
print_success "Database recreated"
else
print_status "Keeping existing database"
fi
else
psql -c "CREATE DATABASE codeforge;"
print_success "Database 'codeforge' created"
fi
# Enable pgvector extension in the database
print_status "Enabling pgvector extension in codeforge database..."
psql -d codeforge -c "CREATE EXTENSION IF NOT EXISTS vector;" &> /dev/null
print_success "pgvector extension enabled in codeforge database"
# Run schema.sql
print_status "Creating database schema..."
if [ -f "app/db/schema.sql" ]; then
psql -d codeforge -f app/db/schema.sql &> /dev/null
print_success "Database schema created"
# Verify tables
TABLE_COUNT=$(psql -d codeforge -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public';" | xargs)
print_success "Created $TABLE_COUNT tables"
else
print_error "Schema file not found: app/db/schema.sql"
exit 1
fi
# ============================================================================
# 5. Check Ollama
# ============================================================================
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${PURPLE} Step 5: Ollama${NC}"
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
if command -v ollama &> /dev/null; then
OLLAMA_VERSION=$(ollama --version 2>&1 | head -n1 || echo "unknown")
print_success "Ollama installed ($OLLAMA_VERSION)"
else
print_error "Ollama not found"
print_status "Installing Ollama..."
if command -v brew &> /dev/null; then
brew install ollama
print_success "Ollama installed via Homebrew"
else
print_status "Please install Ollama from: https://ollama.ai/download"
exit 1
fi
fi
# ============================================================================
# 6. Download Required Models
# ============================================================================
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${PURPLE} Step 6: Ollama Models${NC}"
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
print_status "This will download ~7GB of models. This may take a while..."
# Start Ollama service if not running
if ! pgrep -x "ollama" > /dev/null; then
print_status "Starting Ollama service..."
ollama serve &> /dev/null &
sleep 3
print_success "Ollama service started"
fi
# Download embedding model
print_status "Downloading nomic-embed-text (274 MB)..."
ollama pull nomic-embed-text
print_success "nomic-embed-text downloaded"
# Download LLM model
print_status "Downloading llama3.2:3b (2.0 GB)..."
ollama pull llama3.2:3b
print_success "llama3.2:3b downloaded"
# List installed models
print_success "Installed models:"
ollama list
# ============================================================================
# 7. Create Data Directories
# ============================================================================
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${PURPLE} Step 7: Data Directories${NC}"
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
print_status "Creating data directories..."
mkdir -p data/ollama
print_success "Created data/ollama/"
# ============================================================================
# 8. Build Docker Containers
# ============================================================================
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${PURPLE} Step 8: Docker Containers${NC}"
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
print_status "Building Docker containers..."
docker-compose build
print_success "Docker containers built"
# ============================================================================
# 9. Start Services
# ============================================================================
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${PURPLE} Step 9: Start Services${NC}"
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
print_status "Starting Code-Forge services..."
docker-compose up -d
sleep 5
print_success "Services started"
# ============================================================================
# 10. Verify Installation
# ============================================================================
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${PURPLE} Step 10: Verification${NC}"
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
print_status "Verifying installation..."
# Check if services are running
if docker ps | grep -q code-forge-app; then
print_success "Code-Forge app container running"
else
print_error "Code-Forge app container not running"
fi
if docker ps | grep -q code-forge-ollama; then
print_success "Ollama container running"
else
print_error "Ollama container not running"
fi
# ============================================================================
# Setup Complete!
# ============================================================================
echo -e "\n${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN} ✓ Setup Complete!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
echo -e "${BLUE}🎉 Code-Forge is ready to use!${NC}\n"
echo -e "${YELLOW}Access the application:${NC}"
echo -e " ${GREEN}•${NC} React UI: ${BLUE}http://localhost:5173${NC} ${PURPLE}(Recommended)${NC}"
echo -e " ${GREEN}•${NC} Streamlit UI: ${BLUE}http://localhost:8501${NC}"
echo -e " ${GREEN}•${NC} MCP Server: ${BLUE}http://localhost:8051${NC}"
echo -e " ${GREEN}•${NC} Ollama API: ${BLUE}http://localhost:11434${NC}\n"
echo -e "${YELLOW}Next steps:${NC}"
echo -e " ${GREEN}1.${NC} Start the React frontend: ${BLUE}cd frontend && npm install && npm run dev${NC}"
echo -e " ${GREEN}2.${NC} Open ${BLUE}http://localhost:5173${NC} in your browser"
echo -e " ${GREEN}3.${NC} Create your first knowledge base"
echo -e " ${GREEN}4.${NC} Upload documents and start chatting!\n"
echo -e "${YELLOW}Useful commands:${NC}"
echo -e " ${GREEN}•${NC} View logs: ${BLUE}docker-compose logs -f${NC}"
echo -e " ${GREEN}•${NC} Stop services: ${BLUE}docker-compose down${NC}"
echo -e " ${GREEN}•${NC} Restart services: ${BLUE}docker-compose restart${NC}\n"
echo -e "${PURPLE}For more information, see README.md${NC}\n"