GodRad commited on
Commit
ec77441
·
verified ·
1 Parent(s): 5d12662

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +28 -0
  2. core/config.py +41 -0
  3. core/ingestion.py +61 -0
  4. core/model.py +59 -0
  5. main.py +77 -0
  6. requirements.txt +16 -0
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ curl \
9
+ software-properties-common \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Install Python dependencies
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copy application code
17
+ COPY . .
18
+
19
+ # Create user with UID 1000 (standard for HF Spaces)
20
+ RUN useradd -m -u 1000 user
21
+ USER user
22
+ ENV HOME=/home/user \
23
+ PATH=/home/user/.local/bin:$PATH
24
+
25
+ # Expose the standard HF port
26
+ EXPOSE 7860
27
+
28
+ CMD ["python", "main.py"]
core/config.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pydantic_settings import BaseSettings
3
+ from typing import Optional
4
+
5
+ class Settings(BaseSettings):
6
+ # App Settings
7
+ APP_NAME: str = "Open Source Syllabus Engine"
8
+ DEBUG: bool = False
9
+ PORT: int = 7860
10
+
11
+ # Storage Settings
12
+ PERSISTENT_STORAGE_PATH: str = os.getenv("PERSISTENT_STORAGE_PATH", "/data")
13
+ CHROMA_DB_PATH: str = os.path.join(PERSISTENT_STORAGE_PATH, "chromadb")
14
+ UPLOAD_DIR: str = os.path.join(PERSISTENT_STORAGE_PATH, "uploads")
15
+
16
+ # Model Settings
17
+ # For CPU: unsloth/Llama-3.2-1B-Instruct-GGUF (or similar small GGUF)
18
+ # For GPU: meta-llama/Llama-3.1-8B-Instruct
19
+ MODEL_ID_GPU: str = "meta-llama/Meta-Llama-3.1-8B-Instruct"
20
+ MODEL_ID_CPU: str = "bartowski/Llama-3.2-1B-Instruct-GGUF"
21
+ GGUF_FILE: str = "Llama-3.2-1B-Instruct-Q4_K_M.gguf"
22
+
23
+ DEVICE: str = os.getenv("DEVICE", "cpu") # "cpu" or "cuda"
24
+
25
+ # Security
26
+ API_KEY: Optional[str] = os.getenv("API_KEY") # Secret set in HF Spaces
27
+ HF_TOKEN: Optional[str] = os.getenv("HF_TOKEN")
28
+
29
+ # Constraints
30
+ MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 10MB
31
+ CHUNK_SIZE: int = 1000
32
+ CHUNK_OVERLAP: int = 200
33
+
34
+ class Config:
35
+ env_file = ".env"
36
+
37
+ settings = Settings()
38
+
39
+ # Ensure directories exist
40
+ os.makedirs(settings.UPLOAD_DIR, exist_ok=True)
41
+ os.makedirs(settings.CHROMA_DB_PATH, exist_ok=True)
core/ingestion.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import hashlib
3
+ from typing import List
4
+ import pypdf
5
+ from docx import Document
6
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
7
+ from langchain_community.vectorstores import Chroma
8
+ from langchain_huggingface import HuggingFaceEmbeddings
9
+ from .config import settings
10
+
11
+ class DocumentProcessor:
12
+ def __init__(self):
13
+ self.embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
14
+ self.vector_store = Chroma(
15
+ persist_directory=settings.CHROMA_DB_PATH,
16
+ embedding_function=self.embeddings
17
+ )
18
+ self.text_splitter = RecursiveCharacterTextSplitter(
19
+ chunk_size=settings.CHUNK_SIZE,
20
+ chunk_overlap=settings.CHUNK_OVERLAP
21
+ )
22
+
23
+ def get_file_checksum(self, file_path: str) -> str:
24
+ with open(file_path, "rb") as f:
25
+ return hashlib.md5(f.read()).hexdigest()
26
+
27
+ def extract_text(self, file_path: str) -> str:
28
+ ext = os.path.splitext(file_path)[1].lower()
29
+ text = ""
30
+ if ext == ".pdf":
31
+ with open(file_path, "rb") as f:
32
+ reader = pypdf.PdfReader(f)
33
+ for page in reader.pages:
34
+ text += page.extract_text() + "\n"
35
+ elif ext == ".docx":
36
+ doc = Document(file_path)
37
+ for para in doc.paragraphs:
38
+ text += para.text + "\n"
39
+ elif ext == ".txt":
40
+ with open(file_path, "r", encoding="utf-8") as f:
41
+ text = f.read()
42
+ return text
43
+
44
+ def ingest_document(self, file_path: str, metadata: dict = None):
45
+ text = self.extract_text(file_path)
46
+ if not text.strip():
47
+ return None
48
+
49
+ chunks = self.text_splitter.split_text(text)
50
+ metadatas = [metadata or {} for _ in chunks]
51
+ for meta in metadatas:
52
+ meta["source"] = os.path.basename(file_path)
53
+ meta["checksum"] = self.get_file_checksum(file_path)
54
+
55
+ self.vector_store.add_texts(texts=chunks, metadatas=metadatas)
56
+ return len(chunks)
57
+
58
+ def search(self, query: str, k: int = 5):
59
+ return self.vector_store.similarity_search(query, k=k)
60
+
61
+ processor = DocumentProcessor()
core/model.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
3
+ from llama_cpp import Llama
4
+ import os
5
+ from .config import settings
6
+
7
+ class SyllabusGenerator:
8
+ def __init__(self):
9
+ self.model = None
10
+ self.tokenizer = None
11
+ self.is_gpu = settings.DEVICE == "cuda" and torch.cuda.is_available()
12
+ self.load_model()
13
+
14
+ def load_model(self):
15
+ if self.is_gpu:
16
+ print(f"Loading GPU model: {settings.MODEL_ID_GPU}")
17
+ self.tokenizer = AutoTokenizer.from_pretrained(settings.MODEL_ID_GPU)
18
+ self.model = AutoModelForCausalLM.from_pretrained(
19
+ settings.MODEL_ID_GPU,
20
+ device_map="auto",
21
+ torch_dtype=torch.bfloat16,
22
+ load_in_4bit=True
23
+ )
24
+ else:
25
+ print(f"Loading CPU model (GGUF): {settings.MODEL_ID_CPU}")
26
+ # Ensure model file is downloaded
27
+ # In a real HF space, these would be pre-cached or downloaded on init
28
+ self.llm = Llama.from_pretrained(
29
+ repo_id=settings.MODEL_ID_CPU,
30
+ filename=settings.GGUF_FILE,
31
+ n_ctx=2048,
32
+ n_threads=os.cpu_count() or 4
33
+ )
34
+
35
+ def generate(self, prompt: str, context: str = "", max_tokens: int = 1000):
36
+ full_prompt = f"""
37
+ System: You are an expert academic curriculum designer.
38
+ Use the following context to generate a detailed syllabus.
39
+ Context: {context}
40
+
41
+ Question: {prompt}
42
+
43
+ Syllabus Output:
44
+ """
45
+
46
+ if self.is_gpu:
47
+ inputs = self.tokenizer(full_prompt, return_tensors="pt").to("cuda")
48
+ outputs = self.model.generate(**inputs, max_new_tokens=max_tokens)
49
+ return self.tokenizer.decode(outputs[0], skip_special_tokens=True)
50
+ else:
51
+ response = self.llm(
52
+ full_prompt,
53
+ max_tokens=max_tokens,
54
+ stop=["Question:", "System:"],
55
+ echo=False
56
+ )
57
+ return response["choices"][0]["text"]
58
+
59
+ generator = SyllabusGenerator()
main.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ from typing import List, Optional
4
+ from fastapi import FastAPI, UploadFile, File, Header, HTTPException, Depends
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from pydantic import BaseModel
7
+ from .core.config import settings
8
+ from .core.ingestion import processor
9
+ from .core.model import generator
10
+
11
+ app = FastAPI(title=settings.APP_NAME)
12
+
13
+ # CORS configuration
14
+ app.add_middleware(
15
+ CORSMiddleware,
16
+ allow_origins=["*"],
17
+ allow_methods=["*"],
18
+ allow_headers=["*"],
19
+ )
20
+
21
+ # Authentication Dependency
22
+ def verify_api_key(x_api_key: Optional[str] = Header(None)):
23
+ if settings.API_KEY and x_api_key != settings.API_KEY:
24
+ raise HTTPException(status_code=403, detail="Invalid API Key")
25
+ return x_api_key
26
+
27
+ class GenerateRequest(BaseModel):
28
+ course_name: str
29
+ description: str
30
+ constraints: Optional[str] = ""
31
+ max_tokens: int = 2000
32
+
33
+ @app.get("/health")
34
+ def health_check():
35
+ return {
36
+ "status": "healthy",
37
+ "device": settings.DEVICE,
38
+ "gpu_available": generator.is_gpu
39
+ }
40
+
41
+ @app.post("/ingest")
42
+ async def ingest_file(file: UploadFile = File(...), api_key: str = Depends(verify_api_key)):
43
+ if file.size > settings.MAX_FILE_SIZE:
44
+ raise HTTPException(status_code=413, detail="File too large")
45
+
46
+ file_path = os.path.join(settings.UPLOAD_DIR, file.filename)
47
+ with open(file_path, "wb") as buffer:
48
+ shutil.copyfileobj(file.file, buffer)
49
+
50
+ chunks = processor.ingest_document(file_path)
51
+ if chunks is None:
52
+ raise HTTPException(status_code=400, detail="Could not extract text from file")
53
+
54
+ return {"filename": file.filename, "chunks_ingested": chunks}
55
+
56
+ @app.post("/generate_syllabus")
57
+ async def generate_syllabus(req: GenerateRequest, api_key: str = Depends(verify_api_key)):
58
+ # 1. Search for relevant context
59
+ query = f"{req.course_name} {req.description}"
60
+ docs = processor.search(query, k=5)
61
+ context = "\n".join([d.page_content for d in docs])
62
+
63
+ # 2. Build prompt
64
+ prompt = f"Design a comprehensive syllabus for a course titled '{req.course_name}'. Description: {req.description}. Constraints: {req.constraints}"
65
+
66
+ # 3. Generate
67
+ syllabus = generator.generate(prompt, context=context, max_tokens=req.max_tokens)
68
+
69
+ return {
70
+ "course_name": req.course_name,
71
+ "syllabus": syllabus,
72
+ "sources": [d.metadata["source"] for d in docs]
73
+ }
74
+
75
+ if __name__ == "__main__":
76
+ import uvicorn
77
+ uvicorn.run(app, host="0.0.0.0", port=settings.PORT)
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ pydantic-settings
5
+ pypdf
6
+ python-docx
7
+ langchain
8
+ langchain-community
9
+ langchain-huggingface
10
+ chromadb
11
+ transformers
12
+ bitsandbytes
13
+ accelerate
14
+ torch
15
+ llama-cpp-python
16
+ huggingface-hub