Compare commits
6 Commits
913875188a
...
rc/langcha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3793d2d32 | ||
|
|
733ffae8cf | ||
|
|
07512409f1 | ||
|
|
12eb110313 | ||
|
|
1a026f76a1 | ||
|
|
da3a464897 |
@@ -19,7 +19,9 @@ OLLAMA_URL=http://192.168.1.14:11434
|
||||
OLLAMA_HOST=http://192.168.1.14:11434
|
||||
|
||||
# ChromaDB Configuration
|
||||
CHROMADB_PATH=/path/to/chromadb
|
||||
# For Docker: This is automatically set to /app/data/chromadb
|
||||
# For local development: Set to a local directory path
|
||||
CHROMADB_PATH=./data/chromadb
|
||||
|
||||
# OpenAI Configuration
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,5 +14,7 @@ wheels/
|
||||
|
||||
# Database files
|
||||
chromadb/
|
||||
chromadb_openai/
|
||||
chroma_db/
|
||||
database/
|
||||
*.db
|
||||
|
||||
13
classifier.py
Normal file
13
classifier.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import os
|
||||
|
||||
from llm import LLMClient
|
||||
|
||||
USE_OPENAI = os.getenv("OLLAMA_URL")
|
||||
|
||||
|
||||
class Classifier:
|
||||
def __init__(self):
|
||||
self.llm_client = LLMClient()
|
||||
|
||||
def classify_query_by_action(self, query):
|
||||
_prompt = "Classify the query into one of the following options: "
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
@@ -17,7 +15,7 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
raggr-backend:
|
||||
raggr:
|
||||
build:
|
||||
context: ./services/raggr
|
||||
dockerfile: Dockerfile.dev
|
||||
@@ -30,7 +28,7 @@ services:
|
||||
- PAPERLESS_TOKEN=${PAPERLESS_TOKEN}
|
||||
- BASE_URL=${BASE_URL}
|
||||
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}
|
||||
- CHROMADB_PATH=/app/chromadb
|
||||
- CHROMADB_PATH=/app/data/chromadb
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
||||
- OIDC_ISSUER=${OIDC_ISSUER}
|
||||
@@ -41,31 +39,28 @@ services:
|
||||
- DATABASE_URL=postgres://raggr:raggr_dev_password@postgres:5432/raggr
|
||||
- FLASK_ENV=development
|
||||
- PYTHONUNBUFFERED=1
|
||||
- NODE_ENV=development
|
||||
- TAVILY_KEY=${TAVILIY_KEY}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
# Mount source code for hot reload
|
||||
- ./services/raggr:/app
|
||||
# Exclude node_modules and Python cache
|
||||
- /app/raggr-frontend/node_modules
|
||||
- /app/__pycache__
|
||||
# Persist data
|
||||
- chromadb_data:/app/chromadb
|
||||
command: sh -c "chmod +x /app/startup-dev.sh && /app/startup-dev.sh"
|
||||
|
||||
raggr-frontend:
|
||||
build:
|
||||
context: ./services/raggr/raggr-frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
# Mount source code for hot reload
|
||||
- ./services/raggr/raggr-frontend:/app
|
||||
# Exclude node_modules to use container's version
|
||||
- /app/node_modules
|
||||
command: sh -c "yarn build && yarn watch:build"
|
||||
- chromadb_data:/app/data/chromadb
|
||||
develop:
|
||||
watch:
|
||||
# Sync+restart on any file change under services/raggr
|
||||
- action: sync+restart
|
||||
path: ./services/raggr
|
||||
target: /app
|
||||
ignore:
|
||||
- __pycache__/
|
||||
- "*.pyc"
|
||||
- "*.pyo"
|
||||
- "*.pyd"
|
||||
- .git/
|
||||
- chromadb/
|
||||
- node_modules/
|
||||
- raggr-frontend/dist/
|
||||
|
||||
volumes:
|
||||
chromadb_data:
|
||||
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
- PAPERLESS_TOKEN=${PAPERLESS_TOKEN}
|
||||
- BASE_URL=${BASE_URL}
|
||||
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}
|
||||
- CHROMADB_PATH=/app/chromadb
|
||||
- CHROMADB_PATH=/app/data/chromadb
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
||||
- OIDC_ISSUER=${OIDC_ISSUER}
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- chromadb_data:/app/chromadb
|
||||
- chromadb_data:/app/data/chromadb
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -2,10 +2,13 @@ FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies and uv
|
||||
# Install system dependencies, Node.js, uv, and yarn
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& npm install -g yarn \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
@@ -18,16 +21,33 @@ COPY pyproject.toml ./
|
||||
# Install Python dependencies using uv
|
||||
RUN uv pip install --system -e .
|
||||
|
||||
# Copy frontend package files and install dependencies
|
||||
COPY raggr-frontend/package.json raggr-frontend/yarn.lock* raggr-frontend/
|
||||
WORKDIR /app/raggr-frontend
|
||||
RUN yarn install
|
||||
|
||||
# Copy application source code
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Build frontend
|
||||
WORKDIR /app/raggr-frontend
|
||||
RUN yarn build
|
||||
|
||||
# Create ChromaDB and database directories
|
||||
WORKDIR /app
|
||||
RUN mkdir -p /app/chromadb /app/database
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
# Make startup script executable
|
||||
RUN chmod +x /app/startup-dev.sh
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONPATH=/app
|
||||
ENV CHROMADB_PATH=/app/chromadb
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# The actual source code will be mounted as a volume
|
||||
# No CMD here - will be specified in docker-compose
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Default command
|
||||
CMD ["/app/startup-dev.sh"]
|
||||
|
||||
97
services/raggr/VECTORSTORE.md
Normal file
97
services/raggr/VECTORSTORE.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Vector Store Management
|
||||
|
||||
This document describes how to manage the ChromaDB vector store used for RAG (Retrieval-Augmented Generation).
|
||||
|
||||
## Configuration
|
||||
|
||||
The vector store location is controlled by the `CHROMADB_PATH` environment variable:
|
||||
|
||||
- **Development (local)**: Set in `.env` to a local path (e.g., `/path/to/chromadb`)
|
||||
- **Docker**: Automatically set to `/app/data/chromadb` and persisted via Docker volume
|
||||
|
||||
## Management Commands
|
||||
|
||||
### CLI (Command Line)
|
||||
|
||||
Use the `manage_vectorstore.py` script for vector store operations:
|
||||
|
||||
```bash
|
||||
# Show statistics
|
||||
python manage_vectorstore.py stats
|
||||
|
||||
# Index documents from Paperless-NGX (incremental)
|
||||
python manage_vectorstore.py index
|
||||
|
||||
# Clear and reindex all documents
|
||||
python manage_vectorstore.py reindex
|
||||
|
||||
# List documents
|
||||
python manage_vectorstore.py list 10
|
||||
python manage_vectorstore.py list 20 --show-content
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
Run commands inside the Docker container:
|
||||
|
||||
```bash
|
||||
# Show statistics
|
||||
docker compose -f docker-compose.dev.yml exec -T raggr python manage_vectorstore.py stats
|
||||
|
||||
# Reindex all documents
|
||||
docker compose -f docker-compose.dev.yml exec -T raggr python manage_vectorstore.py reindex
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The following authenticated endpoints are available:
|
||||
|
||||
- `GET /api/rag/stats` - Get vector store statistics
|
||||
- `POST /api/rag/index` - Trigger indexing of new documents
|
||||
- `POST /api/rag/reindex` - Clear and reindex all documents
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Document Fetching**: Documents are fetched from Paperless-NGX via the API
|
||||
2. **Chunking**: Documents are split into chunks of ~1000 characters with 200 character overlap
|
||||
3. **Embedding**: Chunks are embedded using OpenAI's `text-embedding-3-large` model
|
||||
4. **Storage**: Embeddings are stored in ChromaDB with metadata (filename, document type, date)
|
||||
5. **Retrieval**: User queries are embedded and similar chunks are retrieved for RAG
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Error creating hnsw segment reader"
|
||||
|
||||
This indicates a corrupted index. Solution:
|
||||
|
||||
```bash
|
||||
python manage_vectorstore.py reindex
|
||||
```
|
||||
|
||||
### Empty results
|
||||
|
||||
Check if documents are indexed:
|
||||
|
||||
```bash
|
||||
python manage_vectorstore.py stats
|
||||
```
|
||||
|
||||
If count is 0, run:
|
||||
|
||||
```bash
|
||||
python manage_vectorstore.py index
|
||||
```
|
||||
|
||||
### Different results in Docker vs local
|
||||
|
||||
Docker and local environments use separate ChromaDB instances. To sync:
|
||||
|
||||
1. Index inside Docker: `docker compose exec -T raggr python manage_vectorstore.py reindex`
|
||||
2. Or mount the same volume for both environments
|
||||
|
||||
## Production Considerations
|
||||
|
||||
1. **Volume Persistence**: Use Docker volumes or persistent storage for ChromaDB
|
||||
2. **Backup**: Regularly backup the ChromaDB data directory
|
||||
3. **Reindexing**: Schedule periodic reindexing to keep data fresh
|
||||
4. **Monitoring**: Monitor the `/api/rag/stats` endpoint for document counts
|
||||
@@ -6,6 +6,7 @@ from tortoise.contrib.quart import register_tortoise
|
||||
|
||||
import blueprints.conversation
|
||||
import blueprints.conversation.logic
|
||||
import blueprints.rag
|
||||
import blueprints.users
|
||||
import blueprints.users.models
|
||||
from main import consult_simba_oracle
|
||||
@@ -22,12 +23,12 @@ jwt = JWTManager(app)
|
||||
# Register blueprints
|
||||
app.register_blueprint(blueprints.users.user_blueprint)
|
||||
app.register_blueprint(blueprints.conversation.conversation_blueprint)
|
||||
app.register_blueprint(blueprints.rag.rag_blueprint)
|
||||
|
||||
|
||||
# Database configuration with environment variable support
|
||||
DATABASE_URL = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgres://raggr:raggr_dev_password@localhost:5432/raggr"
|
||||
"DATABASE_URL", "postgres://raggr:raggr_dev_password@localhost:5432/raggr"
|
||||
)
|
||||
|
||||
TORTOISE_CONFIG = {
|
||||
@@ -123,10 +124,17 @@ async def get_messages():
|
||||
}
|
||||
)
|
||||
|
||||
name = conversation.name
|
||||
if len(messages) > 8:
|
||||
name = await blueprints.conversation.logic.rename_conversation(
|
||||
user=user,
|
||||
conversation=conversation,
|
||||
)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"id": str(conversation.id),
|
||||
"name": conversation.name,
|
||||
"name": name,
|
||||
"messages": messages,
|
||||
"created_at": conversation.created_at.isoformat(),
|
||||
"updated_at": conversation.updated_at.isoformat(),
|
||||
|
||||
@@ -1,27 +1,88 @@
|
||||
import datetime
|
||||
|
||||
from quart import Blueprint, jsonify, request
|
||||
from quart_jwt_extended import (
|
||||
jwt_refresh_token_required,
|
||||
get_jwt_identity,
|
||||
jwt_refresh_token_required,
|
||||
)
|
||||
|
||||
from quart import Blueprint, jsonify
|
||||
import blueprints.users.models
|
||||
|
||||
from .agents import main_agent
|
||||
from .logic import (
|
||||
add_message_to_conversation,
|
||||
get_conversation_by_id,
|
||||
rename_conversation,
|
||||
)
|
||||
from .models import (
|
||||
Conversation,
|
||||
PydConversation,
|
||||
PydListConversation,
|
||||
)
|
||||
|
||||
import blueprints.users.models
|
||||
|
||||
conversation_blueprint = Blueprint(
|
||||
"conversation_api", __name__, url_prefix="/api/conversation"
|
||||
)
|
||||
|
||||
|
||||
@conversation_blueprint.post("/query")
|
||||
@jwt_refresh_token_required
|
||||
async def query():
|
||||
current_user_uuid = get_jwt_identity()
|
||||
user = await blueprints.users.models.User.get(id=current_user_uuid)
|
||||
data = await request.get_json()
|
||||
query = data.get("query")
|
||||
conversation_id = data.get("conversation_id")
|
||||
conversation = await get_conversation_by_id(conversation_id)
|
||||
await conversation.fetch_related("messages")
|
||||
await add_message_to_conversation(
|
||||
conversation=conversation,
|
||||
message=query,
|
||||
speaker="user",
|
||||
user=user,
|
||||
)
|
||||
|
||||
# Build conversation history from recent messages (last 10 for context)
|
||||
recent_messages = (
|
||||
conversation.messages[-10:]
|
||||
if len(conversation.messages) > 10
|
||||
else conversation.messages
|
||||
)
|
||||
|
||||
messages_payload = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful cat assistant named Simba that understands veterinary terms. When there are questions to you specifically, they are referring to Simba the cat. Answer the user in as if you were a cat named Simba. Don't act too catlike. Be assertive.\n\nIMPORTANT: When users ask factual questions about Simba's health, medical history, veterinary visits, medications, weight, or any information that would be in documents, you MUST use the simba_search tool to retrieve accurate information before answering. Do not rely on general knowledge - always search the documents for factual questions.",
|
||||
}
|
||||
]
|
||||
|
||||
# Add recent conversation history
|
||||
for msg in recent_messages[:-1]: # Exclude the message we just added
|
||||
role = "user" if msg.speaker == "user" else "assistant"
|
||||
messages_payload.append({"role": role, "content": msg.text})
|
||||
|
||||
# Add current query
|
||||
messages_payload.append({"role": "user", "content": query})
|
||||
|
||||
payload = {"messages": messages_payload}
|
||||
|
||||
response = await main_agent.ainvoke(payload)
|
||||
message = response.get("messages", [])[-1].content
|
||||
await add_message_to_conversation(
|
||||
conversation=conversation,
|
||||
message=message,
|
||||
speaker="simba",
|
||||
user=user,
|
||||
)
|
||||
return jsonify({"response": message})
|
||||
|
||||
|
||||
@conversation_blueprint.route("/<conversation_id>")
|
||||
@jwt_refresh_token_required
|
||||
async def get_conversation(conversation_id: str):
|
||||
conversation = await Conversation.get(id=conversation_id)
|
||||
current_user_uuid = get_jwt_identity()
|
||||
user = await blueprints.users.models.User.get(id=current_user_uuid)
|
||||
await conversation.fetch_related("messages")
|
||||
|
||||
# Manually serialize the conversation with messages
|
||||
@@ -35,11 +96,18 @@ async def get_conversation(conversation_id: str):
|
||||
"created_at": msg.created_at.isoformat(),
|
||||
}
|
||||
)
|
||||
name = conversation.name
|
||||
if len(messages) > 8 and "datetime" in name.lower():
|
||||
name = await rename_conversation(
|
||||
user=user,
|
||||
conversation=conversation,
|
||||
)
|
||||
print(name)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"id": str(conversation.id),
|
||||
"name": conversation.name,
|
||||
"name": name,
|
||||
"messages": messages,
|
||||
"created_at": conversation.created_at.isoformat(),
|
||||
"updated_at": conversation.updated_at.isoformat(),
|
||||
|
||||
78
services/raggr/blueprints/conversation/agents.py
Normal file
78
services/raggr/blueprints/conversation/agents.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain.chat_models import BaseChatModel
|
||||
from langchain.tools import tool
|
||||
from langchain_ollama import ChatOllama
|
||||
from langchain_openai import ChatOpenAI
|
||||
from tavily import AsyncTavilyClient
|
||||
|
||||
from blueprints.rag.logic import query_vector_store
|
||||
|
||||
openai_gpt_5_mini = ChatOpenAI(model="gpt-5-mini")
|
||||
ollama_deepseek = ChatOllama(model="llama3.1:8b", base_url=os.getenv("OLLAMA_URL"))
|
||||
model_with_fallback = cast(
|
||||
BaseChatModel, ollama_deepseek.with_fallbacks([openai_gpt_5_mini])
|
||||
)
|
||||
client = AsyncTavilyClient(os.getenv("TAVILY_KEY"), "")
|
||||
|
||||
|
||||
@tool
|
||||
async def web_search(query: str) -> str:
|
||||
"""Search the web for current information using Tavily.
|
||||
|
||||
Use this tool when you need to:
|
||||
- Find current information not in the knowledge base
|
||||
- Look up recent events, news, or updates
|
||||
- Verify facts or get additional context
|
||||
- Search for information outside of Simba's documents
|
||||
|
||||
Args:
|
||||
query: The search query to look up on the web
|
||||
|
||||
Returns:
|
||||
Search results from the web with titles, content, and source URLs
|
||||
"""
|
||||
response = await client.search(query=query, search_depth="basic")
|
||||
results = response.get("results", [])
|
||||
|
||||
if not results:
|
||||
return "No results found for the query."
|
||||
|
||||
formatted = "\n\n".join(
|
||||
[
|
||||
f"**{result['title']}**\n{result['content']}\nSource: {result['url']}"
|
||||
for result in results[:5]
|
||||
]
|
||||
)
|
||||
return formatted
|
||||
|
||||
|
||||
@tool(response_format="content_and_artifact")
|
||||
async def simba_search(query: str):
|
||||
"""Search through Simba's medical records, veterinary documents, and personal information.
|
||||
|
||||
Use this tool whenever the user asks questions about:
|
||||
- Simba's health history, medical records, or veterinary visits
|
||||
- Medications, treatments, or diagnoses
|
||||
- Weight, diet, or physical characteristics over time
|
||||
- Veterinary recommendations or advice
|
||||
- Ryan's (the owner's) information related to Simba
|
||||
- Any factual information that would be found in documents
|
||||
|
||||
Args:
|
||||
query: The user's question or information need about Simba
|
||||
|
||||
Returns:
|
||||
Relevant information from Simba's documents
|
||||
"""
|
||||
print(f"[SIMBA SEARCH] Tool called with query: {query}")
|
||||
serialized, docs = await query_vector_store(query=query)
|
||||
print(f"[SIMBA SEARCH] Found {len(docs)} documents")
|
||||
print(f"[SIMBA SEARCH] Serialized result length: {len(serialized)}")
|
||||
print(f"[SIMBA SEARCH] First 200 chars: {serialized[:200]}")
|
||||
return serialized, docs
|
||||
|
||||
|
||||
main_agent = create_agent(model=model_with_fallback, tools=[simba_search, web_search])
|
||||
@@ -1,9 +1,10 @@
|
||||
import tortoise.exceptions
|
||||
|
||||
from .models import Conversation, ConversationMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
import blueprints.users.models
|
||||
|
||||
from .models import Conversation, ConversationMessage, RenameConversationOutputSchema
|
||||
|
||||
|
||||
async def create_conversation(name: str = "") -> Conversation:
|
||||
conversation = await Conversation.create(name=name)
|
||||
@@ -58,3 +59,22 @@ async def get_conversation_transcript(
|
||||
messages.append(f"{message.speaker} at {message.created_at}: {message.text}")
|
||||
|
||||
return "\n".join(messages)
|
||||
|
||||
|
||||
async def rename_conversation(
|
||||
user: blueprints.users.models.User,
|
||||
conversation: Conversation,
|
||||
) -> str:
|
||||
messages: str = await get_conversation_transcript(
|
||||
user=user, conversation=conversation
|
||||
)
|
||||
|
||||
llm = ChatOpenAI(model="gpt-4o-mini")
|
||||
structured_llm = llm.with_structured_output(RenameConversationOutputSchema)
|
||||
|
||||
prompt = f"Summarize the following conversation into a sassy one-liner title:\n\n{messages}"
|
||||
response = structured_llm.invoke(prompt)
|
||||
new_name: str = response.get("title", "")
|
||||
conversation.name = new_name
|
||||
await conversation.save()
|
||||
return new_name
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
from tortoise.models import Model
|
||||
from tortoise import fields
|
||||
from tortoise.contrib.pydantic import (
|
||||
pydantic_queryset_creator,
|
||||
pydantic_model_creator,
|
||||
pydantic_queryset_creator,
|
||||
)
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenameConversationOutputSchema:
|
||||
title: str
|
||||
justification: str
|
||||
|
||||
|
||||
class Speaker(enum.Enum):
|
||||
|
||||
46
services/raggr/blueprints/rag/__init__.py
Normal file
46
services/raggr/blueprints/rag/__init__.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from quart import Blueprint, jsonify
|
||||
from quart_jwt_extended import jwt_refresh_token_required
|
||||
|
||||
from .logic import get_vector_store_stats, index_documents, vector_store
|
||||
|
||||
rag_blueprint = Blueprint("rag_api", __name__, url_prefix="/api/rag")
|
||||
|
||||
|
||||
@rag_blueprint.get("/stats")
|
||||
@jwt_refresh_token_required
|
||||
async def get_stats():
|
||||
"""Get vector store statistics."""
|
||||
stats = get_vector_store_stats()
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
@rag_blueprint.post("/index")
|
||||
@jwt_refresh_token_required
|
||||
async def trigger_index():
|
||||
"""Trigger indexing of documents from Paperless-NGX."""
|
||||
try:
|
||||
await index_documents()
|
||||
stats = get_vector_store_stats()
|
||||
return jsonify({"status": "success", "stats": stats})
|
||||
except Exception as e:
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
|
||||
@rag_blueprint.post("/reindex")
|
||||
@jwt_refresh_token_required
|
||||
async def trigger_reindex():
|
||||
"""Clear and reindex all documents."""
|
||||
try:
|
||||
# Clear existing documents
|
||||
collection = vector_store._collection
|
||||
all_docs = collection.get()
|
||||
|
||||
if all_docs["ids"]:
|
||||
collection.delete(ids=all_docs["ids"])
|
||||
|
||||
# Reindex
|
||||
await index_documents()
|
||||
stats = get_vector_store_stats()
|
||||
return jsonify({"status": "success", "stats": stats})
|
||||
except Exception as e:
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
75
services/raggr/blueprints/rag/fetchers.py
Normal file
75
services/raggr/blueprints/rag/fetchers.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class PaperlessNGXService:
|
||||
def __init__(self):
|
||||
self.base_url = os.getenv("BASE_URL")
|
||||
self.token = os.getenv("PAPERLESS_TOKEN")
|
||||
self.url = f"http://{os.getenv('BASE_URL')}/api/documents/?tags__id=8"
|
||||
self.headers = {"Authorization": f"Token {os.getenv('PAPERLESS_TOKEN')}"}
|
||||
|
||||
def get_data(self):
|
||||
print(f"Getting data from: {self.url}")
|
||||
r = httpx.get(self.url, headers=self.headers)
|
||||
results = r.json()["results"]
|
||||
|
||||
nextLink = r.json().get("next")
|
||||
|
||||
while nextLink:
|
||||
r = httpx.get(nextLink, headers=self.headers)
|
||||
results += r.json()["results"]
|
||||
nextLink = r.json().get("next")
|
||||
|
||||
return results
|
||||
|
||||
def get_doc_by_id(self, doc_id: int):
|
||||
url = f"http://{os.getenv('BASE_URL')}/api/documents/{doc_id}/"
|
||||
r = httpx.get(url, headers=self.headers)
|
||||
return r.json()
|
||||
|
||||
def download_pdf_from_id(self, id: int) -> str:
|
||||
download_url = f"http://{os.getenv('BASE_URL')}/api/documents/{id}/download/"
|
||||
response = httpx.get(
|
||||
download_url, headers=self.headers, follow_redirects=True, timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
# Use a temporary file for the downloaded PDF
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
|
||||
temp_file.write(response.content)
|
||||
temp_file.close()
|
||||
temp_pdf_path = temp_file.name
|
||||
pdf_to_process = temp_pdf_path
|
||||
return pdf_to_process
|
||||
|
||||
def upload_cleaned_content(self, document_id, data):
|
||||
PUTS_URL = f"http://{os.getenv('BASE_URL')}/api/documents/{document_id}/"
|
||||
r = httpx.put(PUTS_URL, headers=self.headers, data=data)
|
||||
r.raise_for_status()
|
||||
|
||||
def upload_description(self, description_filepath, file, title, exif_date: str):
|
||||
POST_URL = f"http://{os.getenv('BASE_URL')}/api/documents/post_document/"
|
||||
files = {"document": ("description_filepath", file, "application/txt")}
|
||||
data = {
|
||||
"title": title,
|
||||
"create": exif_date,
|
||||
"document_type": 3,
|
||||
"tags": [7],
|
||||
}
|
||||
|
||||
r = httpx.post(POST_URL, headers=self.headers, data=data, files=files)
|
||||
r.raise_for_status()
|
||||
|
||||
def get_tags(self):
|
||||
GET_URL = f"http://{os.getenv('BASE_URL')}/api/tags/"
|
||||
r = httpx.get(GET_URL, headers=self.headers)
|
||||
data = r.json()
|
||||
return {tag["id"]: tag["name"] for tag in data["results"]}
|
||||
|
||||
def get_doctypes(self):
|
||||
GET_URL = f"http://{os.getenv('BASE_URL')}/api/document_types/"
|
||||
r = httpx.get(GET_URL, headers=self.headers)
|
||||
data = r.json()
|
||||
return {doctype["id"]: doctype["name"] for doctype in data["results"]}
|
||||
101
services/raggr/blueprints/rag/logic.py
Normal file
101
services/raggr/blueprints/rag/logic.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from langchain_chroma import Chroma
|
||||
from langchain_core.documents import Document
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||
|
||||
from .fetchers import PaperlessNGXService
|
||||
|
||||
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
|
||||
|
||||
vector_store = Chroma(
|
||||
collection_name="simba_docs",
|
||||
embedding_function=embeddings,
|
||||
persist_directory=os.getenv("CHROMADB_PATH", ""),
|
||||
)
|
||||
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=1000, # chunk size (characters)
|
||||
chunk_overlap=200, # chunk overlap (characters)
|
||||
add_start_index=True, # track index in original document
|
||||
)
|
||||
|
||||
|
||||
def date_to_epoch(date_str: str) -> float:
|
||||
split_date = date_str.split("-")
|
||||
date = datetime.datetime(
|
||||
int(split_date[0]),
|
||||
int(split_date[1]),
|
||||
int(split_date[2]),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
return date.timestamp()
|
||||
|
||||
|
||||
async def fetch_documents_from_paperless_ngx() -> list[Document]:
|
||||
ppngx = PaperlessNGXService()
|
||||
data = ppngx.get_data()
|
||||
doctypes = ppngx.get_doctypes()
|
||||
documents = []
|
||||
for doc in data:
|
||||
metadata = {
|
||||
"created_date": date_to_epoch(doc["created_date"]),
|
||||
"filename": doc["original_file_name"],
|
||||
"document_type": doctypes.get(doc["document_type"], ""),
|
||||
}
|
||||
documents.append(Document(page_content=doc["content"], metadata=metadata))
|
||||
|
||||
return documents
|
||||
|
||||
|
||||
async def index_documents():
|
||||
documents = await fetch_documents_from_paperless_ngx()
|
||||
|
||||
splits = text_splitter.split_documents(documents)
|
||||
await vector_store.aadd_documents(documents=splits)
|
||||
|
||||
|
||||
async def query_vector_store(query: str):
|
||||
retrieved_docs = await vector_store.asimilarity_search(query, k=2)
|
||||
serialized = "\n\n".join(
|
||||
(f"Source: {doc.metadata}\nContent: {doc.page_content}")
|
||||
for doc in retrieved_docs
|
||||
)
|
||||
return serialized, retrieved_docs
|
||||
|
||||
|
||||
def get_vector_store_stats():
|
||||
"""Get statistics about the vector store."""
|
||||
collection = vector_store._collection
|
||||
count = collection.count()
|
||||
return {
|
||||
"total_documents": count,
|
||||
"collection_name": collection.name,
|
||||
}
|
||||
|
||||
|
||||
def list_all_documents(limit: int = 10):
|
||||
"""List documents in the vector store with their metadata."""
|
||||
collection = vector_store._collection
|
||||
results = collection.get(limit=limit, include=["metadatas", "documents"])
|
||||
|
||||
documents = []
|
||||
for i, doc_id in enumerate(results["ids"]):
|
||||
documents.append(
|
||||
{
|
||||
"id": doc_id,
|
||||
"metadata": results["metadatas"][i]
|
||||
if results.get("metadatas")
|
||||
else None,
|
||||
"content_preview": results["documents"][i][:200]
|
||||
if results.get("documents")
|
||||
else None,
|
||||
}
|
||||
)
|
||||
|
||||
return documents
|
||||
0
services/raggr/blueprints/rag/models.py
Normal file
0
services/raggr/blueprints/rag/models.py
Normal file
@@ -1,18 +1,16 @@
|
||||
import httpx
|
||||
import os
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import tempfile
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from image_process import describe_simba_image
|
||||
from request import PaperlessNGXService
|
||||
import sqlite3
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configuration from environment variables
|
||||
@@ -89,7 +87,7 @@ if __name__ == "__main__":
|
||||
image_date = description.image_date
|
||||
|
||||
description_filepath = os.path.join(
|
||||
"/Users/ryanchen/Programs/raggr", f"SIMBA_DESCRIBE_001.txt"
|
||||
"/Users/ryanchen/Programs/raggr", "SIMBA_DESCRIBE_001.txt"
|
||||
)
|
||||
file = open(description_filepath, "w+")
|
||||
file.write(image_description)
|
||||
|
||||
92
services/raggr/inspect_vector_store.py
Normal file
92
services/raggr/inspect_vector_store.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI tool to inspect the vector store contents."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from blueprints.rag.logic import (
|
||||
get_vector_store_stats,
|
||||
index_documents,
|
||||
list_all_documents,
|
||||
)
|
||||
|
||||
# Load .env from the root directory
|
||||
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
env_path = os.path.join(root_dir, ".env")
|
||||
load_dotenv(env_path)
|
||||
|
||||
|
||||
def print_stats():
|
||||
"""Print vector store statistics."""
|
||||
stats = get_vector_store_stats()
|
||||
print("=== Vector Store Statistics ===")
|
||||
print(f"Collection Name: {stats['collection_name']}")
|
||||
print(f"Total Documents: {stats['total_documents']}")
|
||||
print()
|
||||
|
||||
|
||||
def print_documents(limit: int = 10, show_content: bool = False):
|
||||
"""Print documents in the vector store."""
|
||||
docs = list_all_documents(limit=limit)
|
||||
print(f"=== Documents (showing {len(docs)} of {limit} requested) ===\n")
|
||||
|
||||
for i, doc in enumerate(docs, 1):
|
||||
print(f"Document {i}:")
|
||||
print(f" ID: {doc['id']}")
|
||||
print(f" Metadata: {doc['metadata']}")
|
||||
if show_content:
|
||||
print(f" Content Preview: {doc['content_preview']}")
|
||||
print()
|
||||
|
||||
|
||||
async def run_index():
|
||||
"""Run the indexing process."""
|
||||
print("Starting indexing process...")
|
||||
await index_documents()
|
||||
print("Indexing complete!")
|
||||
print_stats()
|
||||
|
||||
|
||||
def main():
|
||||
import asyncio
|
||||
|
||||
parser = argparse.ArgumentParser(description="Inspect the vector store contents")
|
||||
parser.add_argument(
|
||||
"--stats", action="store_true", help="Show vector store statistics"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list", type=int, metavar="N", help="List N documents from the vector store"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--show-content",
|
||||
action="store_true",
|
||||
help="Show content preview when listing documents",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--index",
|
||||
action="store_true",
|
||||
help="Index documents from Paperless-NGX into the vector store",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Handle indexing first if requested
|
||||
if args.index:
|
||||
asyncio.run(run_index())
|
||||
return
|
||||
|
||||
# If no arguments provided, show stats by default
|
||||
if not any([args.stats, args.list]):
|
||||
args.stats = True
|
||||
|
||||
if args.stats:
|
||||
print_stats()
|
||||
|
||||
if args.list:
|
||||
print_documents(limit=args.list, show_content=args.show_content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,23 +1,19 @@
|
||||
import argparse
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
import argparse
|
||||
import chromadb
|
||||
import ollama
|
||||
|
||||
import time
|
||||
|
||||
import ollama
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from request import PaperlessNGXService
|
||||
import chromadb
|
||||
from chunker import Chunker
|
||||
from cleaner import pdf_to_image, summarize_pdf_image
|
||||
from llm import LLMClient
|
||||
from query import QueryGenerator
|
||||
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from request import PaperlessNGXService
|
||||
|
||||
_dotenv_loaded = load_dotenv()
|
||||
|
||||
|
||||
121
services/raggr/manage_vectorstore.py
Normal file
121
services/raggr/manage_vectorstore.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Management script for vector store operations."""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from blueprints.rag.logic import (
|
||||
get_vector_store_stats,
|
||||
index_documents,
|
||||
list_all_documents,
|
||||
vector_store,
|
||||
)
|
||||
|
||||
|
||||
def stats():
|
||||
"""Show vector store statistics."""
|
||||
stats = get_vector_store_stats()
|
||||
print("=== Vector Store Statistics ===")
|
||||
print(f"Collection: {stats['collection_name']}")
|
||||
print(f"Total Documents: {stats['total_documents']}")
|
||||
|
||||
|
||||
async def index():
|
||||
"""Index documents from Paperless-NGX."""
|
||||
print("Starting indexing process...")
|
||||
print("Fetching documents from Paperless-NGX...")
|
||||
await index_documents()
|
||||
print("✓ Indexing complete!")
|
||||
stats()
|
||||
|
||||
|
||||
async def reindex():
|
||||
"""Clear and reindex all documents."""
|
||||
print("Clearing existing documents...")
|
||||
collection = vector_store._collection
|
||||
all_docs = collection.get()
|
||||
|
||||
if all_docs["ids"]:
|
||||
print(f"Deleting {len(all_docs['ids'])} existing documents...")
|
||||
collection.delete(ids=all_docs["ids"])
|
||||
print("✓ Cleared")
|
||||
else:
|
||||
print("Collection is already empty")
|
||||
|
||||
await index()
|
||||
|
||||
|
||||
def list_docs(limit: int = 10, show_content: bool = False):
|
||||
"""List documents in the vector store."""
|
||||
docs = list_all_documents(limit=limit)
|
||||
print(f"\n=== Documents (showing {len(docs)}) ===\n")
|
||||
|
||||
for i, doc in enumerate(docs, 1):
|
||||
print(f"Document {i}:")
|
||||
print(f" ID: {doc['id']}")
|
||||
print(f" Metadata: {doc['metadata']}")
|
||||
if show_content:
|
||||
print(f" Content: {doc['content_preview']}")
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Manage vector store for RAG system",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s stats # Show vector store statistics
|
||||
%(prog)s index # Index new documents from Paperless-NGX
|
||||
%(prog)s reindex # Clear and reindex all documents
|
||||
%(prog)s list 10 # List first 10 documents
|
||||
%(prog)s list 20 --show-content # List 20 documents with content preview
|
||||
""",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
||||
|
||||
# Stats command
|
||||
subparsers.add_parser("stats", help="Show vector store statistics")
|
||||
|
||||
# Index command
|
||||
subparsers.add_parser("index", help="Index documents from Paperless-NGX")
|
||||
|
||||
# Reindex command
|
||||
subparsers.add_parser("reindex", help="Clear and reindex all documents")
|
||||
|
||||
# List command
|
||||
list_parser = subparsers.add_parser("list", help="List documents in vector store")
|
||||
list_parser.add_argument(
|
||||
"limit", type=int, default=10, nargs="?", help="Number of documents to list"
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--show-content", action="store_true", help="Show content preview"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
if args.command == "stats":
|
||||
stats()
|
||||
elif args.command == "index":
|
||||
asyncio.run(index())
|
||||
elif args.command == "reindex":
|
||||
asyncio.run(reindex())
|
||||
elif args.command == "list":
|
||||
list_docs(limit=args.limit, show_content=args.show_content)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nOperation cancelled by user")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,7 +4,39 @@ version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = ["chromadb>=1.1.0", "python-dotenv>=1.0.0", "flask>=3.1.2", "httpx>=0.28.1", "ollama>=0.6.0", "openai>=2.0.1", "pydantic>=2.11.9", "pillow>=10.0.0", "pymupdf>=1.24.0", "black>=25.9.0", "pillow-heif>=1.1.1", "flask-jwt-extended>=4.7.1", "bcrypt>=5.0.0", "pony>=0.7.19", "flask-login>=0.6.3", "quart>=0.20.0", "tortoise-orm>=0.25.1", "quart-jwt-extended>=0.1.0", "pre-commit>=4.3.0", "tortoise-orm-stubs>=1.0.2", "aerich>=0.8.0", "tomlkit>=0.13.3", "authlib>=1.3.0", "asyncpg>=0.30.0"]
|
||||
dependencies = [
|
||||
"chromadb>=1.1.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
"flask>=3.1.2",
|
||||
"httpx>=0.28.1",
|
||||
"ollama>=0.6.0",
|
||||
"openai>=2.0.1",
|
||||
"pydantic>=2.11.9",
|
||||
"pillow>=10.0.0",
|
||||
"pymupdf>=1.24.0",
|
||||
"black>=25.9.0",
|
||||
"pillow-heif>=1.1.1",
|
||||
"flask-jwt-extended>=4.7.1",
|
||||
"bcrypt>=5.0.0",
|
||||
"pony>=0.7.19",
|
||||
"flask-login>=0.6.3",
|
||||
"quart>=0.20.0",
|
||||
"tortoise-orm>=0.25.1",
|
||||
"quart-jwt-extended>=0.1.0",
|
||||
"pre-commit>=4.3.0",
|
||||
"tortoise-orm-stubs>=1.0.2",
|
||||
"aerich>=0.8.0",
|
||||
"tomlkit>=0.13.3",
|
||||
"authlib>=1.3.0",
|
||||
"asyncpg>=0.30.0",
|
||||
"langchain-openai>=1.1.6",
|
||||
"langchain>=1.2.0",
|
||||
"langchain-chroma>=1.0.0",
|
||||
"langchain-community>=0.4.1",
|
||||
"jq>=1.10.0",
|
||||
"langchain-ollama>=1.0.1",
|
||||
"tavily-python>=0.7.17",
|
||||
]
|
||||
|
||||
[tool.aerich]
|
||||
tortoise_orm = "app.TORTOISE_CONFIG"
|
||||
|
||||
@@ -8,8 +8,11 @@ COPY package.json yarn.lock* ./
|
||||
# Install dependencies
|
||||
RUN yarn install
|
||||
|
||||
# Copy application source code
|
||||
COPY . .
|
||||
|
||||
# Expose rsbuild dev server port (default 3000)
|
||||
EXPOSE 3000
|
||||
|
||||
# The actual source code will be mounted as a volume
|
||||
# CMD will be specified in docker-compose
|
||||
# Default command
|
||||
CMD ["sh", "-c", "yarn build && yarn watch:build"]
|
||||
|
||||
@@ -37,7 +37,7 @@ class ConversationService {
|
||||
conversation_id: string,
|
||||
): Promise<QueryResponse> {
|
||||
const response = await userService.fetchWithRefreshToken(
|
||||
`${this.baseUrl}/query`,
|
||||
`${this.conversationBaseUrl}/query`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ query, conversation_id }),
|
||||
|
||||
@@ -40,6 +40,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
const [selectedConversation, setSelectedConversation] =
|
||||
useState<Conversation | null>(null);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const simbaAnswers = ["meow.", "hiss...", "purrrrrr", "yowOWROWWowowr"];
|
||||
@@ -80,6 +81,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
setConversations(parsedConversations);
|
||||
setSelectedConversation(parsedConversations[0]);
|
||||
console.log(parsedConversations);
|
||||
console.log("JELLYFISH@");
|
||||
} catch (error) {
|
||||
console.error("Failed to load messages:", error);
|
||||
}
|
||||
@@ -104,11 +106,18 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
const loadMessages = async () => {
|
||||
console.log(selectedConversation);
|
||||
console.log("JELLYFISH");
|
||||
if (selectedConversation == null) return;
|
||||
try {
|
||||
const conversation = await conversationService.getConversation(
|
||||
selectedConversation.id,
|
||||
);
|
||||
// Update the conversation title in case it changed
|
||||
setSelectedConversation({
|
||||
id: conversation.id,
|
||||
title: conversation.name,
|
||||
});
|
||||
setMessages(
|
||||
conversation.messages.map((message) => ({
|
||||
text: message.text,
|
||||
@@ -120,14 +129,15 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
}
|
||||
};
|
||||
loadMessages();
|
||||
}, [selectedConversation]);
|
||||
}, [selectedConversation?.id]);
|
||||
|
||||
const handleQuestionSubmit = async () => {
|
||||
if (!query.trim()) return; // Don't submit empty messages
|
||||
if (!query.trim() || isLoading) return; // Don't submit empty messages or while loading
|
||||
|
||||
const currMessages = messages.concat([{ text: query, speaker: "user" }]);
|
||||
setMessages(currMessages);
|
||||
setQuery(""); // Clear input immediately after submission
|
||||
setIsLoading(true);
|
||||
|
||||
if (simbaMode) {
|
||||
console.log("simba mode activated");
|
||||
@@ -142,6 +152,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
},
|
||||
]),
|
||||
);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,6 +173,8 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
if (error instanceof Error && error.message.includes("Session expired")) {
|
||||
setAuthenticated(false);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -180,9 +193,11 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
return (
|
||||
<div className="h-screen flex flex-row bg-[#F9F5EB]">
|
||||
{/* Sidebar - Expanded */}
|
||||
<aside className={`hidden md:flex md:flex-col bg-white border-r border-gray-200 p-4 overflow-y-auto transition-all duration-300 ${sidebarCollapsed ? 'w-20' : 'w-64'}`}>
|
||||
<aside
|
||||
className={`hidden md:flex md:flex-col bg-[#F9F5EB] border-r border-gray-200 p-4 overflow-y-auto transition-all duration-300 ${sidebarCollapsed ? "w-20" : "w-64"}`}
|
||||
>
|
||||
{!sidebarCollapsed ? (
|
||||
<>
|
||||
<div className="bg-[#F9F5EB]">
|
||||
<div className="flex flex-row items-center gap-2 mb-6">
|
||||
<img
|
||||
src={catIcon}
|
||||
@@ -205,7 +220,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
logout
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<img
|
||||
@@ -243,7 +258,18 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
</header>
|
||||
|
||||
{/* Messages area */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
{selectedConversation && (
|
||||
<div className="sticky top-0 mx-auto w-full">
|
||||
<div className="bg-[#F9F5EB] text-black px-6 w-full py-3">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{selectedConversation.title || "Untitled Conversation"}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto relative px-4 py-6">
|
||||
{/* Floating conversation name */}
|
||||
|
||||
<div className="max-w-2xl mx-auto flex flex-col gap-4">
|
||||
{showConversations && (
|
||||
<div className="md:hidden">
|
||||
@@ -260,6 +286,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
}
|
||||
return <QuestionBubble key={index} text={msg.text} />;
|
||||
})}
|
||||
{isLoading && <AnswerBubble text="" loading={true} />}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -273,6 +300,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
handleKeyDown={handleKeyDown}
|
||||
handleQuestionSubmit={handleQuestionSubmit}
|
||||
setSimbaMode={setSimbaMode}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -6,9 +6,17 @@ type MessageInputProps = {
|
||||
handleQuestionSubmit: () => void;
|
||||
setSimbaMode: (sdf: boolean) => void;
|
||||
query: string;
|
||||
}
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export const MessageInput = ({query, handleKeyDown, handleQueryChange, handleQuestionSubmit, setSimbaMode}: MessageInputProps) => {
|
||||
export const MessageInput = ({
|
||||
query,
|
||||
handleKeyDown,
|
||||
handleQueryChange,
|
||||
handleQuestionSubmit,
|
||||
setSimbaMode,
|
||||
isLoading,
|
||||
}: MessageInputProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 sticky bottom-0 bg-[#3D763A] p-6 rounded-xl">
|
||||
<div className="flex flex-row justify-between grow">
|
||||
@@ -23,11 +31,16 @@ export const MessageInput = ({query, handleKeyDown, handleQueryChange, handleQue
|
||||
</div>
|
||||
<div className="flex flex-row justify-between gap-2 grow">
|
||||
<button
|
||||
className="p-3 sm:p-4 min-h-[44px] border border-blue-400 bg-[#EDA541] hover:bg-blue-400 cursor-pointer rounded-md flex-grow text-sm sm:text-base"
|
||||
className={`p-3 sm:p-4 min-h-[44px] border border-blue-400 rounded-md flex-grow text-sm sm:text-base ${
|
||||
isLoading
|
||||
? "bg-gray-400 cursor-not-allowed opacity-50"
|
||||
: "bg-[#EDA541] hover:bg-blue-400 cursor-pointer"
|
||||
}`}
|
||||
onClick={() => handleQuestionSubmit()}
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Submit
|
||||
{isLoading ? "Sending..." : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-row justify-center gap-2 grow items-center">
|
||||
@@ -40,4 +53,4 @@ export const MessageInput = ({query, handleKeyDown, handleQueryChange, handleQue
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
set -e
|
||||
|
||||
echo "Initializing directories..."
|
||||
mkdir -p /app/chromadb
|
||||
mkdir -p /app/data/chromadb
|
||||
|
||||
echo "Waiting for frontend to build..."
|
||||
while [ ! -f /app/raggr-frontend/dist/index.html ]; do
|
||||
sleep 1
|
||||
done
|
||||
echo "Frontend built successfully!"
|
||||
echo "Rebuilding frontend..."
|
||||
cd /app/raggr-frontend
|
||||
yarn build
|
||||
cd /app
|
||||
|
||||
echo "Setting up database..."
|
||||
# Give PostgreSQL a moment to be ready (healthcheck in docker-compose handles this)
|
||||
@@ -22,8 +21,5 @@ else
|
||||
aerich init-db
|
||||
fi
|
||||
|
||||
echo "Starting reindex process..."
|
||||
python main.py "" --reindex || echo "Reindex failed, continuing anyway..."
|
||||
|
||||
echo "Starting Flask application in debug mode..."
|
||||
python app.py
|
||||
|
||||
39
services/raggr/test_query.py
Normal file
39
services/raggr/test_query.py
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test the query_vector_store function."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from blueprints.rag.logic import query_vector_store
|
||||
|
||||
# Load .env from the root directory
|
||||
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
env_path = os.path.join(root_dir, ".env")
|
||||
load_dotenv(env_path)
|
||||
|
||||
|
||||
async def test_query(query: str):
|
||||
"""Test a query against the vector store."""
|
||||
print(f"Query: {query}\n")
|
||||
result, docs = await query_vector_store(query)
|
||||
print(f"Found {len(docs)} documents\n")
|
||||
print("Serialized result:")
|
||||
print(result)
|
||||
print("\n" + "=" * 80 + "\n")
|
||||
|
||||
|
||||
async def main():
|
||||
queries = [
|
||||
"What is Simba's weight?",
|
||||
"What medications is Simba taking?",
|
||||
"Tell me about Simba's recent vet visits",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
await test_query(query)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
1091
services/raggr/uv.lock
generated
1091
services/raggr/uv.lock
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user