Major updates to use react and agent os profiles
This commit is contained in:
parent
8e5a46aa59
commit
a6a9c77f51
48 changed files with 12242 additions and 1237 deletions
|
|
@ -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
14
.gitignore
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -15,3 +15,11 @@ enableCORS = false
|
|||
[browser]
|
||||
gatherUsageStats = false
|
||||
|
||||
[ui]
|
||||
# Sidebar configuration
|
||||
hideTopBar = false
|
||||
hideSidebarNav = false
|
||||
|
||||
[client]
|
||||
toolbarMode = "minimal"
|
||||
|
||||
|
|
|
|||
472
README.md
472
README.md
|
|
@ -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)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](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
BIN
app/assets/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
279
app/core/agent_os_ingestion.py
Normal file
279
app/core/agent_os_ingestion.py
Normal 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
295
app/core/agent_os_parser.py
Normal 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)
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
231
app/core/kb_types.py
Normal 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
348
app/core/pg_manager.py
Normal 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
|
||||
|
||||
|
|
@ -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
95
app/db/schema.sql
Normal 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;
|
||||
|
||||
1006
app/main.py
1006
app/main.py
File diff suppressed because it is too large
Load diff
185
app/pages/0_Welcome.py
Normal file
185
app/pages/0_Welcome.py
Normal 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
851
app/pages/1_Main.py
Normal 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
62
design-guide.md
Normal 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.
|
||||
|
||||
---
|
||||
|
|
@ -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
24
frontend/.gitignore
vendored
Normal 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
73
frontend/README.md
Normal 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
23
frontend/eslint.config.js
Normal 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
13
frontend/index.html
Normal 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
6227
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
42
frontend/package.json
Normal file
42
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
7
frontend/postcss.config.js
Normal file
7
frontend/postcss.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
BIN
frontend/public/assets/hero.png
Normal file
BIN
frontend/public/assets/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/public/assets/logo.png
Normal file
BIN
frontend/public/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 213 KiB |
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal 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
29
frontend/src/App.tsx
Normal 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;
|
||||
1
frontend/src/assets/react.svg
Normal file
1
frontend/src/assets/react.svg
Normal 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 |
225
frontend/src/components/ChatInterface.tsx
Normal file
225
frontend/src/components/ChatInterface.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
262
frontend/src/components/HelpDocs.tsx
Normal file
262
frontend/src/components/HelpDocs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
261
frontend/src/components/KBManagement.tsx
Normal file
261
frontend/src/components/KBManagement.tsx
Normal 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
97
frontend/src/index.css
Normal 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
10
frontend/src/main.tsx
Normal 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>,
|
||||
)
|
||||
285
frontend/src/pages/MainApp.tsx
Normal file
285
frontend/src/pages/MainApp.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
155
frontend/src/pages/Welcome.tsx
Normal file
155
frontend/src/pages/Welcome.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
49
frontend/tailwind.config.js
Normal file
49
frontend/tailwind.config.js
Normal 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: [],
|
||||
}
|
||||
|
||||
28
frontend/tsconfig.app.json
Normal file
28
frontend/tsconfig.app.json
Normal 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
7
frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
frontend/tsconfig.node.json
Normal file
26
frontend/tsconfig.node.json
Normal 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
7
frontend/vite.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
330
setup.sh
Executable 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"
|
||||
|
||||
Loading…
Add table
Reference in a new issue