Spaces:
Sleeping
Sleeping
findEthics
commited on
Commit
·
7d886a8
1
Parent(s):
4b34d35
Initial commit
Browse files- Dockerfile +30 -0
- README.md +1 -0
- app.py +177 -0
- requirements.txt +10 -0
Dockerfile
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use Python 3.9 slim image for efficiency
|
2 |
+
FROM python:3.9-slim
|
3 |
+
|
4 |
+
# Create a non-root user for security
|
5 |
+
RUN useradd -m -u 1000 user
|
6 |
+
USER user
|
7 |
+
|
8 |
+
# Set environment variables
|
9 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
10 |
+
ENV PYTHONPATH="/app:$PYTHONPATH"
|
11 |
+
|
12 |
+
# Set working directory
|
13 |
+
WORKDIR /app
|
14 |
+
|
15 |
+
# Copy requirements file with proper ownership
|
16 |
+
COPY --chown=user requirements.txt requirements.txt
|
17 |
+
|
18 |
+
# Install Python dependencies
|
19 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
20 |
+
pip install --no-cache-dir --upgrade -r requirements.txt
|
21 |
+
|
22 |
+
# Copy application code
|
23 |
+
COPY --chown=user app.py /app/
|
24 |
+
COPY --chown=user . /app/
|
25 |
+
|
26 |
+
# Expose port 7860 for Hugging Face Spaces
|
27 |
+
EXPOSE 7860
|
28 |
+
|
29 |
+
# Command to run the application
|
30 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
@@ -6,6 +6,7 @@ colorTo: pink
|
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
license: mit
|
|
|
9 |
---
|
10 |
|
11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
license: mit
|
9 |
+
app_port: 7860
|
10 |
---
|
11 |
|
12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
from fastapi import FastAPI, HTTPException
|
3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
4 |
+
from pydantic import BaseModel
|
5 |
+
import torch
|
6 |
+
from transformers import pipeline
|
7 |
+
from duckduckgo_search import DDGS
|
8 |
+
from typing import Optional, List, Dict, Any
|
9 |
+
import logging
|
10 |
+
|
11 |
+
# Configure logging
|
12 |
+
logging.basicConfig(level=logging.INFO)
|
13 |
+
logger = logging.getLogger(__name__)
|
14 |
+
|
15 |
+
# Initialize FastAPI app
|
16 |
+
app = FastAPI(
|
17 |
+
title="Open Source Chat API",
|
18 |
+
description="A fully open source alternative to HF API using local models",
|
19 |
+
version="1.0.0"
|
20 |
+
)
|
21 |
+
|
22 |
+
# Configure CORS for Android app access
|
23 |
+
app.add_middleware(
|
24 |
+
CORSMiddleware,
|
25 |
+
allow_origins=["*"],
|
26 |
+
allow_methods=["POST", "GET"],
|
27 |
+
allow_headers=["*"],
|
28 |
+
)
|
29 |
+
|
30 |
+
# Request/Response models
|
31 |
+
class ChatRequest(BaseModel):
|
32 |
+
prompt: str
|
33 |
+
max_new_tokens: int = 500
|
34 |
+
use_search: bool = False
|
35 |
+
temperature: float = 0.7
|
36 |
+
|
37 |
+
class ChatResponse(BaseModel):
|
38 |
+
response: str
|
39 |
+
search_results: Optional[List[Dict[str, Any]]] = None # Contains web search results if use_search is enabled
|
40 |
+
|
41 |
+
class SearchRequest(BaseModel):
|
42 |
+
query: str
|
43 |
+
max_results: int = 5
|
44 |
+
|
45 |
+
qa_pipeline = None
|
46 |
+
|
47 |
+
# Function to load the local language model
|
48 |
+
def load_model():
|
49 |
+
"""Load the local language model"""
|
50 |
+
global qa_pipeline
|
51 |
+
|
52 |
+
try:
|
53 |
+
|
54 |
+
# Check if GPU is available
|
55 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
56 |
+
logger.info(f"Using device: {device}")
|
57 |
+
|
58 |
+
qa_pipeline = pipeline(
|
59 |
+
"question-answering",
|
60 |
+
model="distilbert-base-uncased-distilled-squad",
|
61 |
+
device=0 if device == "cuda" else -1
|
62 |
+
)
|
63 |
+
|
64 |
+
logger.info("Model loaded successfully!")
|
65 |
+
|
66 |
+
except Exception as e:
|
67 |
+
logger.error(f"Error loading model: {e}")
|
68 |
+
|
69 |
+
def search_web(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
|
70 |
+
"""Search the web using DuckDuckGo"""
|
71 |
+
try:
|
72 |
+
ddgs = DDGS()
|
73 |
+
results = []
|
74 |
+
|
75 |
+
for result in ddgs.text(query,safesearch='off',max_results=max_results):
|
76 |
+
results.append({
|
77 |
+
"title": result.get("title", ""),
|
78 |
+
"body": result.get("body", ""),
|
79 |
+
"href": result.get("href", "")
|
80 |
+
})
|
81 |
+
|
82 |
+
return results
|
83 |
+
|
84 |
+
except Exception as e:
|
85 |
+
logger.error(f"Search error: {e}")
|
86 |
+
return []
|
87 |
+
|
88 |
+
def generate_response(prompt: str,search_context: str) -> str:
|
89 |
+
"""Generate response using local model"""
|
90 |
+
try:
|
91 |
+
if qa_pipeline is None:
|
92 |
+
return "Model not loaded properly. Please try again."
|
93 |
+
|
94 |
+
# Validate that qa_pipeline is a question-answering pipeline
|
95 |
+
if not hasattr(qa_pipeline, "task") or qa_pipeline.task != "question-answering":
|
96 |
+
return "Invalid pipeline type. Expected a question-answering pipeline."
|
97 |
+
|
98 |
+
result = qa_pipeline(question=prompt, context=search_context)['answer']
|
99 |
+
return result
|
100 |
+
|
101 |
+
except Exception as e:
|
102 |
+
logger.error(f"Generation error: {e}")
|
103 |
+
return f"Sorry, I encountered an error: {str(e)}"
|
104 |
+
|
105 |
+
@app.on_event("startup")
|
106 |
+
async def startup_event():
|
107 |
+
"""Load model on startup"""
|
108 |
+
load_model()
|
109 |
+
|
110 |
+
@app.get("/")
|
111 |
+
async def root():
|
112 |
+
"""Health check endpoint"""
|
113 |
+
return {
|
114 |
+
"message": "Open Source Chat API is running!",
|
115 |
+
"model_loaded": qa_pipeline is not None,
|
116 |
+
"endpoints": {
|
117 |
+
"chat": "/chat",
|
118 |
+
"search": "/search",
|
119 |
+
"docs": "/docs"
|
120 |
+
}
|
121 |
+
}
|
122 |
+
|
123 |
+
@app.post("/chat", response_model=ChatResponse)
|
124 |
+
async def chat(request: ChatRequest):
|
125 |
+
"""Main chat endpoint"""
|
126 |
+
try:
|
127 |
+
search_results = None
|
128 |
+
|
129 |
+
# Perform web search if requested
|
130 |
+
if request.use_search:
|
131 |
+
search_results = search_web(request.prompt)
|
132 |
+
search_context = None
|
133 |
+
# Enhance prompt with search results
|
134 |
+
if search_results:
|
135 |
+
num_results = min(5, len(search_results))
|
136 |
+
search_context = "\n".join([
|
137 |
+
f"- {result['title']}: {result['body'][:200]}..."
|
138 |
+
for result in search_results[:num_results]
|
139 |
+
])
|
140 |
+
else:
|
141 |
+
search_context = request.prompt
|
142 |
+
|
143 |
+
if search_context:
|
144 |
+
# Generate response
|
145 |
+
response = generate_response(
|
146 |
+
request.prompt,
|
147 |
+
search_context
|
148 |
+
)
|
149 |
+
else:
|
150 |
+
response = "No context available to generate a response."
|
151 |
+
|
152 |
+
return ChatResponse(
|
153 |
+
response=response,
|
154 |
+
search_results=search_results if request.use_search else None
|
155 |
+
)
|
156 |
+
|
157 |
+
except Exception as e:
|
158 |
+
logger.error(f"Chat endpoint error: {e}")
|
159 |
+
raise HTTPException(status_code=500, detail=str(e))
|
160 |
+
|
161 |
+
@app.post("/search")
|
162 |
+
async def search(request: SearchRequest):
|
163 |
+
"""Web search endpoint"""
|
164 |
+
try:
|
165 |
+
results = search_web(request.query, request.max_results)
|
166 |
+
return {"results": results}
|
167 |
+
|
168 |
+
except Exception as e:
|
169 |
+
logger.error(f"Search endpoint error: {e}")
|
170 |
+
raise HTTPException(status_code=500, detail=str(e))
|
171 |
+
|
172 |
+
if __name__ == "__main__":
|
173 |
+
import uvicorn
|
174 |
+
import os
|
175 |
+
host = os.getenv("HOST", "0.0.0.0")
|
176 |
+
port = int(os.getenv("PORT", 7860)) # Changed from 8000 to 7860
|
177 |
+
uvicorn.run(app, host=host, port=port)
|
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi==0.104.1
|
2 |
+
uvicorn[standard]==0.24.0
|
3 |
+
pydantic==2.5.0
|
4 |
+
torch==2.6.0
|
5 |
+
transformers==4.35.0
|
6 |
+
accelerate==0.24.0
|
7 |
+
duckduckgo-search
|
8 |
+
requests==2.31.0
|
9 |
+
python-multipart==0.0.6
|
10 |
+
keras
|