Compare commits
16 Commits
rc/9-metad
...
0bb3e3172b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb3e3172b | ||
|
|
24b30bc8a3 | ||
|
|
3ffc95a1b0 | ||
|
|
c5091dc07a | ||
|
|
c140758560 | ||
|
|
ab3a0eb442 | ||
|
|
c619d78922 | ||
|
|
c20ae0a4b9 | ||
|
|
26cc01b58b | ||
|
|
746b60e070 | ||
|
|
577c9144ac | ||
|
|
2b2891bd79 | ||
|
|
03b033e9a4 | ||
|
|
a640ae5fed | ||
|
|
99c98b7e42 | ||
|
|
a69f7864f3 |
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@@ -0,0 +1,16 @@
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env
|
||||
.DS_Store
|
||||
chromadb/
|
||||
chroma_db/
|
||||
raggr-frontend/node_modules/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
.venv/
|
||||
venv/
|
||||
.pytest_cache/
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
||||
46
Dockerfile
Normal file
46
Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies, Node.js, Yarn, and uv
|
||||
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
|
||||
|
||||
# Add uv to PATH
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
# Copy dependency files
|
||||
COPY pyproject.toml ./
|
||||
|
||||
# Install Python dependencies using uv
|
||||
RUN uv pip install --system -e .
|
||||
|
||||
# Copy application code
|
||||
COPY *.py ./
|
||||
COPY startup.sh ./
|
||||
RUN chmod +x startup.sh
|
||||
|
||||
# Copy frontend code and build
|
||||
COPY raggr-frontend ./raggr-frontend
|
||||
WORKDIR /app/raggr-frontend
|
||||
RUN yarn install && yarn build
|
||||
WORKDIR /app
|
||||
|
||||
# Create ChromaDB directory
|
||||
RUN mkdir -p /app/chromadb
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONPATH=/app
|
||||
ENV CHROMADB_PATH=/app/chromadb
|
||||
|
||||
# Run the startup script
|
||||
CMD ["./startup.sh"]
|
||||
44
app.py
Normal file
44
app.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
|
||||
from flask import Flask, request, jsonify, render_template, send_from_directory
|
||||
|
||||
from main import consult_simba_oracle
|
||||
|
||||
app = Flask(
|
||||
__name__,
|
||||
static_folder="raggr-frontend/dist/static",
|
||||
template_folder="raggr-frontend/dist",
|
||||
)
|
||||
|
||||
|
||||
# Serve React static files
|
||||
@app.route("/static/<path:filename>")
|
||||
def static_files(filename):
|
||||
return send_from_directory(app.static_folder, filename)
|
||||
|
||||
|
||||
# Serve the React app for all routes (catch-all)
|
||||
@app.route("/", defaults={"path": ""})
|
||||
@app.route("/<path:path>")
|
||||
def serve_react_app(path):
|
||||
if path and os.path.exists(os.path.join(app.template_folder, path)):
|
||||
return send_from_directory(app.template_folder, path)
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/api/query", methods=["POST"])
|
||||
def query():
|
||||
data = request.get_json()
|
||||
query = data.get("query")
|
||||
return jsonify({"response": consult_simba_oracle(query)})
|
||||
|
||||
|
||||
@app.route("/api/ingest", methods=["POST"])
|
||||
def webhook():
|
||||
data = request.get_json()
|
||||
print(data)
|
||||
return jsonify({"status": "received"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8080, debug=True)
|
||||
12
chunker.py
12
chunker.py
@@ -4,8 +4,8 @@ import re
|
||||
from typing import Union
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from chromadb.utils.embedding_functions.ollama_embedding_function import (
|
||||
OllamaEmbeddingFunction,
|
||||
from chromadb.utils.embedding_functions.openai_embedding_function import (
|
||||
OpenAIEmbeddingFunction,
|
||||
)
|
||||
from dotenv import load_dotenv
|
||||
|
||||
@@ -80,9 +80,9 @@ class Chunk:
|
||||
|
||||
|
||||
class Chunker:
|
||||
embedding_fx = OllamaEmbeddingFunction(
|
||||
url=os.getenv("OLLAMA_URL", ""),
|
||||
model_name="mxbai-embed-large",
|
||||
embedding_fx = OpenAIEmbeddingFunction(
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
model_name="text-embedding-3-small",
|
||||
)
|
||||
|
||||
def __init__(self, collection) -> None:
|
||||
@@ -96,7 +96,7 @@ class Chunker:
|
||||
) -> list[Chunk]:
|
||||
doc_uuid = uuid4()
|
||||
|
||||
chunk_size = min(chunk_size, len(document))
|
||||
chunk_size = min(chunk_size, len(document)) or 1
|
||||
|
||||
chunks = []
|
||||
num_chunks = ceil(len(document) / chunk_size)
|
||||
|
||||
@@ -12,6 +12,9 @@ from request import PaperlessNGXService
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configure ollama client with URL from environment or default to localhost
|
||||
ollama_client = ollama.Client(host=os.getenv("OLLAMA_URL", "http://localhost:11434"))
|
||||
|
||||
parser = argparse.ArgumentParser(description="use llm to clean documents")
|
||||
parser.add_argument("document_id", type=str, help="questions about simba's health")
|
||||
|
||||
@@ -131,7 +134,7 @@ Someone will kill the innocent kittens if you don't extract the text exactly. So
|
||||
|
||||
|
||||
def summarize_pdf_image(filepaths: list[str]):
|
||||
res = ollama.chat(
|
||||
res = ollama_client.chat(
|
||||
model="gemma3:4b",
|
||||
messages=[
|
||||
{
|
||||
|
||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
raggr:
|
||||
image: torrtle/simbarag:latest
|
||||
network_mode: host
|
||||
environment:
|
||||
- PAPERLESS_TOKEN=${PAPERLESS_TOKEN}
|
||||
- BASE_URL=${BASE_URL}
|
||||
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}
|
||||
- CHROMADB_PATH=/app/chromadb
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
volumes:
|
||||
- chromadb_data:/app/chromadb
|
||||
|
||||
volumes:
|
||||
chromadb_data:
|
||||
81
image_process.py
Normal file
81
image_process.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from ollama import Client
|
||||
import argparse
|
||||
import os
|
||||
import logging
|
||||
from PIL import Image, ExifTags
|
||||
from pillow_heif import register_heif_opener
|
||||
from pydantic import BaseModel
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
register_heif_opener()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="SimbaImageProcessor",
|
||||
description="What the program does",
|
||||
epilog="Text at the bottom of help",
|
||||
)
|
||||
|
||||
parser.add_argument("filepath")
|
||||
|
||||
client = Client(host=os.getenv("OLLAMA_HOST", "http://localhost:11434"))
|
||||
|
||||
class SimbaImageDescription(BaseModel):
|
||||
image_date: str
|
||||
description: str
|
||||
|
||||
def describe_simba_image(input):
|
||||
logging.info("Opening image of Simba ...")
|
||||
if "heic" in input.lower() or "heif" in input.lower():
|
||||
new_filepath = input.split(".")[0] + ".jpg"
|
||||
img = Image.open(input)
|
||||
img.save(new_filepath, 'JPEG')
|
||||
logging.info("Extracting EXIF...")
|
||||
exif = {
|
||||
ExifTags.TAGS[k]: v for k, v in img.getexif().items() if k in ExifTags.TAGS
|
||||
}
|
||||
img = Image.open(new_filepath)
|
||||
input=new_filepath
|
||||
else:
|
||||
img = Image.open(input)
|
||||
|
||||
logging.info("Extracting EXIF...")
|
||||
exif = {
|
||||
ExifTags.TAGS[k]: v for k, v in img.getexif().items() if k in ExifTags.TAGS
|
||||
}
|
||||
|
||||
if "MakerNote" in exif:
|
||||
exif.pop("MakerNote")
|
||||
|
||||
logging.info(exif)
|
||||
|
||||
prompt = f"Simba is an orange cat belonging to Ryan Chen. In 2025, they lived in New York. In 2024, they lived in California. Analyze the following image and tell me what Simba seems to be doing. Be extremely descriptive about Simba, things in the background, and the setting of the image. I will also include the EXIF data of the image, please use it to help you determine information about Simba. EXIF: {exif}. Put the notes in the description field and the date in the image_date field."
|
||||
|
||||
logging.info("Sending info to Ollama ...")
|
||||
response = client.chat(
|
||||
model="gemma3:4b",
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "you are a very shrewd and descriptive note taker. all of your responses will be formatted like notes in bullet points. be very descriptive. do not leave a single thing out.",
|
||||
},
|
||||
{"role": "user", "content": prompt, "images": [input]},
|
||||
],
|
||||
format=SimbaImageDescription.model_json_schema()
|
||||
)
|
||||
|
||||
result = SimbaImageDescription.model_validate_json(response["message"]["content"])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parser.parse_args()
|
||||
if args.filepath:
|
||||
logging.info
|
||||
describe_simba_image(input=args.filepath)
|
||||
98
index_immich.py
Normal file
98
index_immich.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import httpx
|
||||
import os
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
from image_process import describe_simba_image
|
||||
from request import PaperlessNGXService
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configuration from environment variables
|
||||
IMMICH_URL = os.getenv("IMMICH_URL", "http://localhost:2283")
|
||||
API_KEY = os.getenv("IMMICH_API_KEY")
|
||||
PERSON_NAME = os.getenv("PERSON_NAME", "Simba") # Name of the tagged person/pet
|
||||
DOWNLOAD_DIR = os.getenv("DOWNLOAD_DIR", "./simba_photos")
|
||||
|
||||
# Set up headers
|
||||
headers = {"x-api-key": API_KEY, "Content-Type": "application/json"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ppngx = PaperlessNGXService()
|
||||
people_url = f"{IMMICH_URL}/api/search/person?name=Simba"
|
||||
people = httpx.get(people_url, headers=headers).json()
|
||||
|
||||
simba_id = people[0]["id"]
|
||||
|
||||
ids = {}
|
||||
|
||||
asset_search = f"{IMMICH_URL}/api/search/smart"
|
||||
request_body = {"query": "orange cat"}
|
||||
results = httpx.post(asset_search, headers=headers, json=request_body)
|
||||
|
||||
assets = results.json()["assets"]
|
||||
for asset in assets["items"]:
|
||||
if asset["type"] == "IMAGE":
|
||||
ids[asset["id"]] = asset.get("originalFileName")
|
||||
nextPage = assets.get("nextPage")
|
||||
|
||||
# while nextPage != None:
|
||||
# logging.info(f"next page: {nextPage}")
|
||||
# request_body["page"] = nextPage
|
||||
# results = httpx.post(asset_search, headers=headers, json=request_body)
|
||||
# assets = results.json()["assets"]
|
||||
|
||||
# for asset in assets["items"]:
|
||||
# if asset["type"] == "IMAGE":
|
||||
# ids.add(asset['id'])
|
||||
|
||||
# nextPage = assets.get("nextPage")
|
||||
|
||||
asset_search = f"{IMMICH_URL}/api/search/smart"
|
||||
request_body = {"query": "simba"}
|
||||
results = httpx.post(asset_search, headers=headers, json=request_body)
|
||||
print(results.json()["assets"]["total"])
|
||||
for asset in results.json()["assets"]["items"]:
|
||||
if asset["type"] == "IMAGE":
|
||||
ids[asset["id"]] = asset.get("originalFileName")
|
||||
|
||||
immich_asset_id = list(ids.keys())[1]
|
||||
immich_filename = ids.get(immich_asset_id)
|
||||
response = httpx.get(
|
||||
f"{IMMICH_URL}/api/assets/{immich_asset_id}/original", headers=headers
|
||||
)
|
||||
|
||||
path = os.path.join("/Users/ryanchen/Programs/raggr", immich_filename)
|
||||
file = open(path, "wb+")
|
||||
for chunk in response.iter_bytes(chunk_size=8192):
|
||||
file.write(chunk)
|
||||
|
||||
logging.info("Processing image ...")
|
||||
description = describe_simba_image(path)
|
||||
|
||||
image_description = description.description
|
||||
image_date = description.image_date
|
||||
|
||||
description_filepath = os.path.join("/Users/ryanchen/Programs/raggr", f"SIMBA_DESCRIBE_001.txt")
|
||||
file = open(description_filepath, "w+")
|
||||
file.write(image_description)
|
||||
file.close()
|
||||
|
||||
file = open(description_filepath, 'rb')
|
||||
|
||||
ppngx.upload_description(description_filepath=description_filepath, file=file, title="SIMBA_DESCRIBE_001.txt", exif_date=image_date)
|
||||
|
||||
|
||||
file.close()
|
||||
|
||||
|
||||
|
||||
logging.info("Processing complete. Deleting file.")
|
||||
os.remove(file.name)
|
||||
123
main.py
123
main.py
@@ -6,6 +6,7 @@ from typing import Any, Union
|
||||
import argparse
|
||||
import chromadb
|
||||
import ollama
|
||||
from openai import OpenAI
|
||||
|
||||
|
||||
from request import PaperlessNGXService
|
||||
@@ -17,6 +18,9 @@ from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configure ollama client with URL from environment or default to localhost
|
||||
ollama_client = ollama.Client(host=os.getenv("OLLAMA_URL", "http://localhost:11434"))
|
||||
|
||||
client = chromadb.PersistentClient(path=os.getenv("CHROMADB_PATH", ""))
|
||||
simba_docs = client.get_or_create_collection(name="simba_docs")
|
||||
feline_vet_lookup = client.get_or_create_collection(name="feline_vet_lookup")
|
||||
@@ -29,9 +33,12 @@ parser.add_argument("query", type=str, help="questions about simba's health")
|
||||
parser.add_argument(
|
||||
"--reindex", action="store_true", help="re-index the simba documents"
|
||||
)
|
||||
parser.add_argument("--index", help="index a file")
|
||||
|
||||
ppngx = PaperlessNGXService()
|
||||
|
||||
openai_client = OpenAI()
|
||||
|
||||
|
||||
def index_using_pdf_llm():
|
||||
files = ppngx.get_data()
|
||||
@@ -39,6 +46,7 @@ def index_using_pdf_llm():
|
||||
document_id = file["id"]
|
||||
pdf_path = ppngx.download_pdf_from_id(id=document_id)
|
||||
image_paths = pdf_to_image(filepath=pdf_path)
|
||||
print(f"summarizing {file}")
|
||||
generated_summary = summarize_pdf_image(filepaths=image_paths)
|
||||
file["content"] = generated_summary
|
||||
|
||||
@@ -68,8 +76,10 @@ def chunk_data(docs: list[dict[str, Union[str, Any]]], collection):
|
||||
print(docs)
|
||||
texts: list[str] = [doc["content"] for doc in docs]
|
||||
for index, text in enumerate(texts):
|
||||
print(docs[index]["original_file_name"])
|
||||
metadata = {
|
||||
"created_date": date_to_epoch(docs[index]["created_date"]),
|
||||
"filename": docs[index]["original_file_name"],
|
||||
}
|
||||
chunker.chunk_document(
|
||||
document=text,
|
||||
@@ -77,27 +87,76 @@ def chunk_data(docs: list[dict[str, Union[str, Any]]], collection):
|
||||
)
|
||||
|
||||
|
||||
def chunk_text(texts: list[str], collection):
|
||||
chunker = Chunker(collection)
|
||||
|
||||
for index, text in enumerate(texts):
|
||||
metadata = {}
|
||||
chunker.chunk_document(
|
||||
document=text,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
||||
def consult_oracle(input: str, collection):
|
||||
print(input)
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Ask
|
||||
qg = QueryGenerator()
|
||||
metadata_filter = qg.get_query("input")
|
||||
print(metadata_filter)
|
||||
# print("Starting query generation")
|
||||
# qg_start = time.time()
|
||||
# qg = QueryGenerator()
|
||||
# metadata_filter = qg.get_query(input)
|
||||
# qg_end = time.time()
|
||||
# print(f"Query generation took {qg_end - qg_start:.2f} seconds")
|
||||
# print(metadata_filter)
|
||||
|
||||
print("Starting embedding generation")
|
||||
embedding_start = time.time()
|
||||
embeddings = Chunker.embedding_fx(input=[input])
|
||||
embedding_end = time.time()
|
||||
print(f"Embedding generation took {embedding_end - embedding_start:.2f} seconds")
|
||||
|
||||
print("Starting collection query")
|
||||
query_start = time.time()
|
||||
results = collection.query(
|
||||
query_texts=[input],
|
||||
query_embeddings=embeddings,
|
||||
where=metadata_filter,
|
||||
# where=metadata_filter,
|
||||
)
|
||||
|
||||
print(results)
|
||||
query_end = time.time()
|
||||
print(f"Collection query took {query_end - query_start:.2f} seconds")
|
||||
|
||||
# Generate
|
||||
output = ollama.generate(
|
||||
model="gemma3n:e4b",
|
||||
prompt=f"You are a helpful assistant that understandings veterinary terms. Using the following data, help answer the user's query by providing as many details as possible. Using this data: {results}. Respond to this prompt: {input}",
|
||||
print("Starting LLM generation")
|
||||
llm_start = time.time()
|
||||
# output = ollama_client.generate(
|
||||
# model="gemma3n:e4b",
|
||||
# prompt=f"You are a helpful assistant that understandings veterinary terms. Using the following data, help answer the user's query by providing as many details as possible. Using this data: {results}. Respond to this prompt: {input}",
|
||||
# )
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant that understands veterinary terms.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Using the following data, help answer the user's query by providing as many details as possible. Using this data: {results}. Respond to this prompt: {input}",
|
||||
},
|
||||
],
|
||||
)
|
||||
llm_end = time.time()
|
||||
print(f"LLM generation took {llm_end - llm_start:.2f} seconds")
|
||||
|
||||
print(output["response"])
|
||||
total_time = time.time() - start_time
|
||||
print(f"Total consult_oracle execution took {total_time:.2f} seconds")
|
||||
|
||||
return response.choices[0].message.content
|
||||
|
||||
|
||||
def paperless_workflow(input):
|
||||
@@ -109,24 +168,46 @@ def paperless_workflow(input):
|
||||
consult_oracle(input, simba_docs)
|
||||
|
||||
|
||||
def consult_simba_oracle(input: str):
|
||||
return consult_oracle(
|
||||
input=input,
|
||||
collection=simba_docs,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parser.parse_args()
|
||||
if args.reindex:
|
||||
# logging.info(msg="Fetching documents from Paperless-NGX")
|
||||
# ppngx = PaperlessNGXService()
|
||||
# docs = ppngx.get_data()
|
||||
# logging.info(msg=f"Fetched {len(docs)} documents")
|
||||
print("Fetching documents from Paperless-NGX")
|
||||
ppngx = PaperlessNGXService()
|
||||
docs = ppngx.get_data()
|
||||
print(docs)
|
||||
print(f"Fetched {len(docs)} documents")
|
||||
#
|
||||
# logging.info(msg="Chunking documents now ...")
|
||||
# chunk_data(docs, collection=simba_docs)
|
||||
# logging.info(msg="Done chunking documents")
|
||||
index_using_pdf_llm()
|
||||
print("Chunking documents now ...")
|
||||
chunk_data(docs, collection=simba_docs)
|
||||
print("Done chunking documents")
|
||||
# index_using_pdf_llm()
|
||||
|
||||
if args.index:
|
||||
with open(args.index) as file:
|
||||
extension = args.index.split(".")[-1]
|
||||
|
||||
if extension == "pdf":
|
||||
pdf_path = ppngx.download_pdf_from_id(id=document_id)
|
||||
image_paths = pdf_to_image(filepath=pdf_path)
|
||||
print(f"summarizing {file}")
|
||||
generated_summary = summarize_pdf_image(filepaths=image_paths)
|
||||
elif extension in [".md", ".txt"]:
|
||||
chunk_text(texts=[file.readall()], collection=simba_docs)
|
||||
|
||||
if args.query:
|
||||
logging.info("Consulting oracle ...")
|
||||
consult_oracle(
|
||||
input=args.query,
|
||||
collection=simba_docs,
|
||||
print("Consulting oracle ...")
|
||||
print(
|
||||
consult_oracle(
|
||||
input=args.query,
|
||||
collection=simba_docs,
|
||||
)
|
||||
)
|
||||
else:
|
||||
print("please provide a query")
|
||||
|
||||
@@ -4,4 +4,16 @@ version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
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",
|
||||
]
|
||||
|
||||
43
query.py
43
query.py
@@ -1,10 +1,16 @@
|
||||
import json
|
||||
import os
|
||||
from typing import Literal
|
||||
import datetime
|
||||
from ollama import chat, ChatResponse
|
||||
from ollama import chat, ChatResponse, Client
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Configure ollama client with URL from environment or default to localhost
|
||||
ollama_client = Client(host=os.getenv("OLLAMA_URL", "http://localhost:11434"))
|
||||
|
||||
# This uses inferred filters — which means using LLM to create the metadata filters
|
||||
|
||||
|
||||
@@ -28,10 +34,16 @@ class GeneratedQuery(BaseModel):
|
||||
extracted_metadata_fields: str
|
||||
|
||||
|
||||
class Time(BaseModel):
|
||||
time: int
|
||||
|
||||
|
||||
PROMPT = """
|
||||
You are an information specialist that processes user queries. The current year is 2025. The user queries are all about
|
||||
a cat, Simba, and its records. The types of records are listed below. Using the query, extract the
|
||||
the date range the user is trying to query. You should return the it as a JSON. The date tag is created_date. Return the date in epoch time
|
||||
the date range the user is trying to query. You should return it as a JSON. The date tag is created_date. Return the date in epoch time.
|
||||
|
||||
If the created_date cannot be ascertained, set it to epoch time start.
|
||||
|
||||
|
||||
You have several operators at your disposal:
|
||||
@@ -90,18 +102,31 @@ class QueryGenerator:
|
||||
return date.timestamp()
|
||||
|
||||
def get_query(self, input: str):
|
||||
response: ChatResponse = chat(
|
||||
model="gemma3n:e4b",
|
||||
messages=[
|
||||
client = OpenAI()
|
||||
print(input)
|
||||
response = client.responses.parse(
|
||||
model="gpt-4o",
|
||||
input=[
|
||||
{"role": "system", "content": PROMPT},
|
||||
{"role": "user", "content": input},
|
||||
],
|
||||
format=GeneratedQuery.model_json_schema(),
|
||||
text_format=Time,
|
||||
)
|
||||
print(response)
|
||||
query = json.loads(response.output_parsed.extracted_metadata_fields)
|
||||
|
||||
query = json.loads(
|
||||
json.loads(response["message"]["content"])["extracted_metadata_fields"]
|
||||
)
|
||||
# response: ChatResponse = ollama_client.chat(
|
||||
# model="gemma3n:e4b",
|
||||
# messages=[
|
||||
# {"role": "system", "content": PROMPT},
|
||||
# {"role": "user", "content": input},
|
||||
# ],
|
||||
# format=GeneratedQuery.model_json_schema(),
|
||||
# )
|
||||
|
||||
# query = json.loads(
|
||||
# json.loads(response["message"]["content"])["extracted_metadata_fields"]
|
||||
# )
|
||||
date_key = list(query["created_date"].keys())[0]
|
||||
query["created_date"][date_key] = self.date_to_epoch(
|
||||
query["created_date"][date_key]
|
||||
|
||||
16
raggr-frontend/.gitignore
vendored
Normal file
16
raggr-frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Local
|
||||
.DS_Store
|
||||
*.local
|
||||
*.log*
|
||||
|
||||
# Dist
|
||||
node_modules
|
||||
dist/
|
||||
|
||||
# Profile
|
||||
.rspack-profile-*/
|
||||
|
||||
# IDE
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
36
raggr-frontend/README.md
Normal file
36
raggr-frontend/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Rsbuild project
|
||||
|
||||
## Setup
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Get started
|
||||
|
||||
Start the dev server, and the app will be available at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Build the app for production:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Preview the production build locally:
|
||||
|
||||
```bash
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
To learn more about Rsbuild, check out the following resources:
|
||||
|
||||
- [Rsbuild documentation](https://rsbuild.rs) - explore Rsbuild features and APIs.
|
||||
- [Rsbuild GitHub repository](https://github.com/web-infra-dev/rsbuild) - your feedback and contributions are welcome!
|
||||
26
raggr-frontend/package.json
Normal file
26
raggr-frontend/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "raggr-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rsbuild build",
|
||||
"dev": "rsbuild dev --open",
|
||||
"preview": "rsbuild preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"marked": "^16.3.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-markdown": "^10.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rsbuild/core": "^1.5.6",
|
||||
"@rsbuild/plugin-react": "^1.4.0",
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
5
raggr-frontend/postcss.config.mjs
Normal file
5
raggr-frontend/postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
6
raggr-frontend/rsbuild.config.ts
Normal file
6
raggr-frontend/rsbuild.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from '@rsbuild/core';
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [pluginReact()],
|
||||
});
|
||||
BIN
raggr-frontend/src/.App.tsx.swp
Normal file
BIN
raggr-frontend/src/.App.tsx.swp
Normal file
Binary file not shown.
6
raggr-frontend/src/App.css
Normal file
6
raggr-frontend/src/App.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
91
raggr-frontend/src/App.tsx
Normal file
91
raggr-frontend/src/App.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useState } from "react";
|
||||
import axios from "axios";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
const App = () => {
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [answer, setAnswer] = useState<string>("");
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [simbaMode, setSimbaMode] = useState<boolean>(false);
|
||||
|
||||
const simbaAnswers = ["meow.", "hiss...", "purrrrrr", "yowOWROWWowowr"];
|
||||
|
||||
const handleQuestionSubmit = () => {
|
||||
if (simbaMode) {
|
||||
console.log("simba mode activated");
|
||||
setLoading(true);
|
||||
const randomIndex = Math.floor(Math.random() * simbaAnswers.length);
|
||||
const randomElement = simbaAnswers[randomIndex];
|
||||
setAnswer(randomElement);
|
||||
setTimeout(() => setLoading(false), 3500);
|
||||
return;
|
||||
}
|
||||
const payload = { query: query };
|
||||
setLoading(true);
|
||||
axios
|
||||
.post("/api/query", payload)
|
||||
.then((result) => setAnswer(result.data.response))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
const handleQueryChange = (event) => {
|
||||
setQuery(event.target.value);
|
||||
};
|
||||
return (
|
||||
<div className="bg-[url('./simba_cute.jpeg')] bg-cover bg-center bg-no-repeat h-screen bg-opacity-20">
|
||||
<div className="bg-white/85 h-screen">
|
||||
<div className="flex flex-row justify-center py-4">
|
||||
<div className="flex flex-col gap-4 min-w-xl max-w-xl">
|
||||
<div className="flex flex-row justify-center gap-2 grow">
|
||||
<h1 className="text-3xl">ask simba!</h1>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between gap-2 grow">
|
||||
<textarea
|
||||
type="text"
|
||||
className="p-4 border border-blue-200 rounded-md grow bg-white"
|
||||
onChange={handleQueryChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between gap-2 grow">
|
||||
<button
|
||||
className="p-4 border border-blue-400 bg-blue-200 hover:bg-blue-400 cursor-pointer rounded-md flex-grow"
|
||||
onClick={() => handleQuestionSubmit()}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-row justify-center gap-2 grow">
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={(event) =>
|
||||
setSimbaMode(event.target.checked)
|
||||
}
|
||||
/>
|
||||
<p>simba mode?</p>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex flex-col w-full animate-pulse gap-2">
|
||||
<div className="flex flex-row gap-2 w-full">
|
||||
<div className="bg-gray-400 w-1/2 p-3 rounded-lg" />
|
||||
<div className="bg-gray-400 w-1/2 p-3 rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 w-full">
|
||||
<div className="bg-gray-400 w-1/3 p-3 rounded-lg" />
|
||||
<div className="bg-gray-400 w-2/3 p-3 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
<ReactMarkdown>{answer}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
11
raggr-frontend/src/env.d.ts
vendored
Normal file
11
raggr-frontend/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/// <reference types="@rsbuild/core/types" />
|
||||
|
||||
/**
|
||||
* Imports the SVG file as a React component.
|
||||
* @requires [@rsbuild/plugin-svgr](https://npmjs.com/package/@rsbuild/plugin-svgr)
|
||||
*/
|
||||
declare module '*.svg?react' {
|
||||
import type React from 'react';
|
||||
const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
||||
export default ReactComponent;
|
||||
}
|
||||
13
raggr-frontend/src/index.tsx
Normal file
13
raggr-frontend/src/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootEl = document.getElementById('root');
|
||||
if (rootEl) {
|
||||
const root = ReactDOM.createRoot(rootEl);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
}
|
||||
BIN
raggr-frontend/src/simba_cute.jpeg
Normal file
BIN
raggr-frontend/src/simba_cute.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
BIN
raggr-frontend/src/simba_troll.jpeg
Normal file
BIN
raggr-frontend/src/simba_troll.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
25
raggr-frontend/tsconfig.json
Normal file
25
raggr-frontend/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "ES2020"],
|
||||
"jsx": "react-jsx",
|
||||
"target": "ES2020",
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"useDefineForClassFields": true,
|
||||
|
||||
/* modules */
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"moduleResolution": "bundler",
|
||||
"verbatimModuleSyntax": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
/* type checking */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
1424
raggr-frontend/yarn.lock
Normal file
1424
raggr-frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
23
request.py
23
request.py
@@ -11,8 +11,8 @@ 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/?query=simba"
|
||||
self.headers = {"Authorization": f"Token {os.getenv("PAPERLESS_TOKEN")}"}
|
||||
self.url = f"http://{os.getenv('BASE_URL')}/api/documents/?query=simba"
|
||||
self.headers = {"Authorization": f"Token {os.getenv('PAPERLESS_TOKEN')}"}
|
||||
|
||||
def get_data(self):
|
||||
print(f"Getting data from: {self.url}")
|
||||
@@ -20,12 +20,12 @@ class PaperlessNGXService:
|
||||
return r.json()["results"]
|
||||
|
||||
def get_doc_by_id(self, doc_id: int):
|
||||
url = f"http://{os.getenv("BASE_URL")}/api/documents/{doc_id}/"
|
||||
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/"
|
||||
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
|
||||
)
|
||||
@@ -39,10 +39,23 @@ class PaperlessNGXService:
|
||||
return pdf_to_process
|
||||
|
||||
def upload_cleaned_content(self, document_id, data):
|
||||
PUTS_URL = f"http://{os.getenv("BASE_URL")}/api/documents/{document_id}/"
|
||||
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()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pp = PaperlessNGXService()
|
||||
|
||||
7
startup.sh
Normal file
7
startup.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Starting reindex process..."
|
||||
python main.py "" --reindex
|
||||
|
||||
echo "Starting Flask application..."
|
||||
python app.py
|
||||
Reference in New Issue
Block a user