Spaces:
Running
Running
Add FastAPI application with WebSocket support and authentication
Browse files- .gitignore +1 -0
- Dockerfile +27 -0
- auth.py +63 -0
- docs/diagrams/system.md +28 -0
- docs/diagrams/system.pdf +0 -0
- gen.py +13 -0
- main.py +44 -0
- middleware.py +18 -0
- requirements.txt +27 -0
- schemas.py +49 -0
- static/index.html +666 -0
- static/script.js +44 -0
- static/style.css +172 -0
- utils.py +0 -0
.gitignore
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
.venv
|
Dockerfile
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use the official Python 3.9 image
|
2 |
+
FROM python:3.9
|
3 |
+
|
4 |
+
# Set the working directory to /code
|
5 |
+
WORKDIR /code
|
6 |
+
|
7 |
+
# Copy the current directory contents into the container at /code
|
8 |
+
COPY ./requirements.txt /code/requirements.txt
|
9 |
+
|
10 |
+
# Install requirements.txt
|
11 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
12 |
+
|
13 |
+
# Set up a new user named "user" with user ID 1000
|
14 |
+
RUN useradd -m -u 1000 user
|
15 |
+
# Switch to the "user" user
|
16 |
+
USER user
|
17 |
+
# Set home to the user's home directory
|
18 |
+
ENV HOME=/home/user \
|
19 |
+
PATH=/home/user/.local/bin:$PATH
|
20 |
+
|
21 |
+
# Set the working directory to the user's home directory
|
22 |
+
WORKDIR $HOME/app
|
23 |
+
|
24 |
+
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
25 |
+
COPY --chown=user . $HOME/app
|
26 |
+
|
27 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
auth.py
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import Depends, HTTPException, status
|
2 |
+
from fastapi.security import OAuth2PasswordBearer
|
3 |
+
from jose import JWTError, jwt
|
4 |
+
from passlib.context import CryptContext
|
5 |
+
from datetime import datetime, timedelta
|
6 |
+
import os
|
7 |
+
|
8 |
+
# Load secrets from environment variables
|
9 |
+
SECRET_KEY = os.getenv("SECRET_KEY")
|
10 |
+
ALGORITHM = "HS512"
|
11 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
12 |
+
|
13 |
+
# Password hashing
|
14 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
15 |
+
|
16 |
+
# OAuth2 scheme
|
17 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
18 |
+
|
19 |
+
# Mock user database (replace with a real database in production)
|
20 |
+
fake_users_db = {
|
21 |
+
"admin": {
|
22 |
+
"username": "admin",
|
23 |
+
"hashed_password": pwd_context.hash(os.getenv("ADMIN_PASSWORD")),
|
24 |
+
}
|
25 |
+
}
|
26 |
+
|
27 |
+
def verify_password(plain_password, hashed_password):
|
28 |
+
return pwd_context.verify(plain_password, hashed_password)
|
29 |
+
|
30 |
+
def authenticate_user(username: str, password: str):
|
31 |
+
user = fake_users_db.get(username)
|
32 |
+
if not user or not verify_password(password, user["hashed_password"]):
|
33 |
+
return False
|
34 |
+
return user
|
35 |
+
|
36 |
+
def create_access_token(data: dict, expires_delta: timedelta = None):
|
37 |
+
to_encode = data.copy()
|
38 |
+
if expires_delta:
|
39 |
+
expire = datetime.utcnow() + expires_delta
|
40 |
+
else:
|
41 |
+
expire = datetime.utcnow() + timedelta(minutes=15)
|
42 |
+
to_encode.update({"exp": expire})
|
43 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
44 |
+
return encoded_jwt
|
45 |
+
|
46 |
+
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
47 |
+
credentials_exception = HTTPException(
|
48 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
49 |
+
detail="Could not validate credentials",
|
50 |
+
headers={"WWW-Authenticate": "Bearer"},
|
51 |
+
)
|
52 |
+
try:
|
53 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
54 |
+
username: str = payload.get("sub")
|
55 |
+
if username is None:
|
56 |
+
raise credentials_exception
|
57 |
+
except JWTError:
|
58 |
+
raise credentials_exception
|
59 |
+
|
60 |
+
user = fake_users_db.get(username)
|
61 |
+
if user is None:
|
62 |
+
raise credentials_exception
|
63 |
+
return user
|
docs/diagrams/system.md
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```mermaid
|
2 |
+
graph TD
|
3 |
+
A[Client] -->|HTTP/WebSocket| B[FastAPI Server]
|
4 |
+
B -->|Authentication| C[Auth Module]
|
5 |
+
B -->|Text Generation| D[Generation Module]
|
6 |
+
B -->|Rate Limiting| E[Rate Limiter]
|
7 |
+
B -->|Logging| F[Logging Middleware]
|
8 |
+
C -->|JWT Token| G[Secrets Manager]
|
9 |
+
D -->|Model Inference| H[Hugging Face Model]
|
10 |
+
E -->|Rate Limit Check| I[Redis/Database]
|
11 |
+
F -->|Logs| J[Log Storage]
|
12 |
+
H -->|Streaming| A
|
13 |
+
|
14 |
+
subgraph FastAPI Server
|
15 |
+
B
|
16 |
+
C
|
17 |
+
D
|
18 |
+
E
|
19 |
+
F
|
20 |
+
end
|
21 |
+
|
22 |
+
subgraph External Services
|
23 |
+
G[Secrets Manager]
|
24 |
+
H[Hugging Face Model]
|
25 |
+
I[Redis/Database]
|
26 |
+
J[Log Storage]
|
27 |
+
end
|
28 |
+
```
|
docs/diagrams/system.pdf
ADDED
Binary file (20.6 kB). View file
|
|
gen.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import WebSocket
|
2 |
+
from transformers import pipeline
|
3 |
+
import asyncio
|
4 |
+
|
5 |
+
# Load the model
|
6 |
+
model_name = "deepseek-ai/DeepSeek-R1-Distill-Llama-8B"
|
7 |
+
generator = pipeline("text-generation", model=model_name)
|
8 |
+
|
9 |
+
async def generate_text_stream(prompt: str, websocket: WebSocket):
|
10 |
+
for i in range(10): # Simulate streaming (replace with actual model inference)
|
11 |
+
chunk = generator(prompt, max_length=i + 10, do_sample=True)[0]["generated_text"]
|
12 |
+
await websocket.send_text(chunk)
|
13 |
+
await asyncio.sleep(0.1) # Simulate delay
|
main.py
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, WebSocket, Depends, HTTPException
|
2 |
+
from auth import get_current_user, authenticate_user, create_access_token
|
3 |
+
from gen import generate_text_stream
|
4 |
+
from fastapi.security import OAuth2PasswordRequestForm
|
5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
6 |
+
from middleware import setup_rate_limiter
|
7 |
+
import os
|
8 |
+
|
9 |
+
app = FastAPI()
|
10 |
+
|
11 |
+
|
12 |
+
# Apply rate limiting middleware
|
13 |
+
setup_rate_limiter(app)
|
14 |
+
|
15 |
+
# CORS middleware
|
16 |
+
app.add_middleware(
|
17 |
+
CORSMiddleware,
|
18 |
+
allow_origins=["*"],
|
19 |
+
allow_credentials=True,
|
20 |
+
allow_methods=["*"],
|
21 |
+
allow_headers=["*"],
|
22 |
+
)
|
23 |
+
|
24 |
+
# Login endpoint
|
25 |
+
@app.post("/token")
|
26 |
+
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
27 |
+
user = authenticate_user(form_data.username, form_data.password)
|
28 |
+
if not user:
|
29 |
+
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
30 |
+
access_token = create_access_token(data={"sub": user["username"]})
|
31 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
32 |
+
|
33 |
+
# WebSocket endpoint for streaming
|
34 |
+
@app.websocket("/generate")
|
35 |
+
async def websocket_generate(websocket: WebSocket, token: str):
|
36 |
+
await websocket.accept()
|
37 |
+
try:
|
38 |
+
user = get_current_user(token)
|
39 |
+
prompt = await websocket.receive_text()
|
40 |
+
await generate_text_stream(prompt, websocket)
|
41 |
+
except Exception as e:
|
42 |
+
await websocket.send_text(f"Error: {str(e)}")
|
43 |
+
finally:
|
44 |
+
await websocket.close()
|
middleware.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import Request
|
2 |
+
from slowapi import Limiter
|
3 |
+
from slowapi.util import get_remote_address
|
4 |
+
from slowapi.middleware import SlowAPIMiddleware
|
5 |
+
|
6 |
+
|
7 |
+
limiter = Limiter(key_func=get_remote_address)
|
8 |
+
|
9 |
+
async def log_requests(request: Request, call_next):
|
10 |
+
print(f"Request: {request.method} {request.url}")
|
11 |
+
response = await call_next(request)
|
12 |
+
print(f"Response: {response.status_code}")
|
13 |
+
return response
|
14 |
+
|
15 |
+
# Apply the rate limiter middleware
|
16 |
+
def setup_rate_limiter(app):
|
17 |
+
app.state.limiter = limiter
|
18 |
+
app.add_middleware(SlowAPIMiddleware)
|
requirements.txt
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# FastAPI and ASGI server
|
2 |
+
fastapi
|
3 |
+
uvicorn
|
4 |
+
|
5 |
+
# Hugging Face Transformers for LLM inference
|
6 |
+
transformers
|
7 |
+
torch # or tensorflow if preferred
|
8 |
+
|
9 |
+
# Authentication and security
|
10 |
+
python-jose[cryptography] # JWT support
|
11 |
+
passlib # Password hashing
|
12 |
+
|
13 |
+
# Rate limiting
|
14 |
+
slowapi
|
15 |
+
|
16 |
+
# WebSocket support
|
17 |
+
websockets
|
18 |
+
|
19 |
+
# Logging and middleware
|
20 |
+
loguru # Optional: for better logging
|
21 |
+
|
22 |
+
# Testing (optional for development)
|
23 |
+
pytest
|
24 |
+
httpx # For async HTTP requests in tests
|
25 |
+
|
26 |
+
# Docker and deployment
|
27 |
+
gunicorn # Optional: for production deployment
|
schemas.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, EmailStr
|
2 |
+
|
3 |
+
# Authentication
|
4 |
+
class Token(BaseModel):
|
5 |
+
access_token: str
|
6 |
+
token_type: str
|
7 |
+
|
8 |
+
class TokenData(BaseModel):
|
9 |
+
username: str | None = None
|
10 |
+
|
11 |
+
class User(BaseModel):
|
12 |
+
username: str
|
13 |
+
email: EmailStr | None = None
|
14 |
+
|
15 |
+
class UserInDB(User):
|
16 |
+
hashed_password: str
|
17 |
+
|
18 |
+
class LoginRequest(BaseModel):
|
19 |
+
username: str
|
20 |
+
password: str
|
21 |
+
|
22 |
+
# Generation
|
23 |
+
class GenerationRequest(BaseModel):
|
24 |
+
prompt: str
|
25 |
+
max_length: int = 100
|
26 |
+
temperature: float = 0.7
|
27 |
+
top_k: int = 50
|
28 |
+
top_p: float = 0.95
|
29 |
+
|
30 |
+
class GenerationResponse(BaseModel):
|
31 |
+
generated_text: str
|
32 |
+
|
33 |
+
# WebSocket
|
34 |
+
class WebSocketMessage(BaseModel):
|
35 |
+
prompt: str
|
36 |
+
|
37 |
+
# Error Handling
|
38 |
+
class HTTPError(BaseModel):
|
39 |
+
detail: str
|
40 |
+
|
41 |
+
class Config:
|
42 |
+
schema_extra = {
|
43 |
+
"example": {"detail": "Error message"},
|
44 |
+
}
|
45 |
+
|
46 |
+
# Rate Limiting
|
47 |
+
class RateLimitResponse(BaseModel):
|
48 |
+
message: str
|
49 |
+
retry_after: int
|
static/index.html
ADDED
@@ -0,0 +1,666 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>LLM Host API Documentation</title>
|
7 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/prismjs/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
|
8 |
+
<link href="/home/style.css" rel="stylesheet" />
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div class="container">
|
12 |
+
<nav class="sidebar">
|
13 |
+
<div class="logo">
|
14 |
+
🔗 LLM Host API
|
15 |
+
</div>
|
16 |
+
<div class="nav-items">
|
17 |
+
<!-- Navigation items will be populated by JavaScript -->
|
18 |
+
</div>
|
19 |
+
</nav>
|
20 |
+
<main class="content">
|
21 |
+
<h1>LLM Host API</h1>
|
22 |
+
|
23 |
+
<section id="endpoints">
|
24 |
+
<h2>Endpoints</h2>
|
25 |
+
|
26 |
+
<div class="endpoint" id="login">
|
27 |
+
<h3>1. Login Endpoint</h3>
|
28 |
+
<p><span class="method">POST</span><span class="endpoint-url">/login</span></p>
|
29 |
+
<p>This endpoint is used to authenticate the user and issue an access token and a refresh token.</p>
|
30 |
+
|
31 |
+
<h4>cURL Request:</h4>
|
32 |
+
<pre><code class="language-bash">curl -X POST https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/login \
|
33 |
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
34 |
+
-d 'username=user123&password=password123'</code></pre>
|
35 |
+
|
36 |
+
<h4>Flutter Implementation:</h4>
|
37 |
+
<pre><code class="language-dart">Future<LoginResponse> login(String username, String password) async {
|
38 |
+
final url = Uri.parse('https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/login');
|
39 |
+
|
40 |
+
try {
|
41 |
+
final response = await http.post(
|
42 |
+
url,
|
43 |
+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
44 |
+
body: {
|
45 |
+
'username': username,
|
46 |
+
'password': password,
|
47 |
+
},
|
48 |
+
);
|
49 |
+
|
50 |
+
if (response.statusCode == 200) {
|
51 |
+
return LoginResponse.fromJson(jsonDecode(response.body));
|
52 |
+
} else {
|
53 |
+
throw Exception('Failed to login: ${response.body}');
|
54 |
+
}
|
55 |
+
} catch (e) {
|
56 |
+
throw Exception('Network error: $e');
|
57 |
+
}
|
58 |
+
}
|
59 |
+
|
60 |
+
// Model class
|
61 |
+
class LoginResponse {
|
62 |
+
final String accessToken;
|
63 |
+
final String refreshToken;
|
64 |
+
final String tokenType;
|
65 |
+
final int expiresIn;
|
66 |
+
|
67 |
+
LoginResponse({
|
68 |
+
required this.accessToken,
|
69 |
+
required this.refreshToken,
|
70 |
+
required this.tokenType,
|
71 |
+
required this.expiresIn,
|
72 |
+
});
|
73 |
+
|
74 |
+
factory LoginResponse.fromJson(Map<String, dynamic> json) {
|
75 |
+
return LoginResponse(
|
76 |
+
accessToken: json['access_token'],
|
77 |
+
refreshToken: json['refresh_token'],
|
78 |
+
tokenType: json['token_type'],
|
79 |
+
expiresIn: json['expires_in'],
|
80 |
+
);
|
81 |
+
}
|
82 |
+
}</code></pre>
|
83 |
+
|
84 |
+
<h4>Response:</h4>
|
85 |
+
<pre><code class="language-json">{
|
86 |
+
"access_token": "your-access-token",
|
87 |
+
"refresh_token": "your-refresh-token",
|
88 |
+
"token_type": "bearer",
|
89 |
+
"expires_in": 1800
|
90 |
+
}</code></pre>
|
91 |
+
</div>
|
92 |
+
|
93 |
+
<div class="endpoint" id="refresh">
|
94 |
+
<h3>2. Refresh Token Endpoint</h3>
|
95 |
+
<p><span class="method">POST</span><span class="endpoint-url">/refresh</span></p>
|
96 |
+
<p>This endpoint is used to refresh the access token using a valid refresh token.</p>
|
97 |
+
|
98 |
+
<h4>cURL Request:</h4>
|
99 |
+
<pre><code class="language-bash">curl -X POST https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/refresh \
|
100 |
+
-H "Content-Type: application/json" \
|
101 |
+
-d '{"refresh_token": "your-refresh-token"}'</code></pre>
|
102 |
+
|
103 |
+
<h4>Flutter Implementation:</h4>
|
104 |
+
<pre><code class="language-dart">Future<LoginResponse> refreshToken(String refreshToken) async {
|
105 |
+
final url = Uri.parse('https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/refresh');
|
106 |
+
|
107 |
+
try {
|
108 |
+
final response = await http.post(
|
109 |
+
url,
|
110 |
+
headers: {'Content-Type': 'application/json'},
|
111 |
+
body: jsonEncode({
|
112 |
+
'refresh_token': refreshToken,
|
113 |
+
}),
|
114 |
+
);
|
115 |
+
|
116 |
+
if (response.statusCode == 200) {
|
117 |
+
return LoginResponse.fromJson(jsonDecode(response.body));
|
118 |
+
} else {
|
119 |
+
throw Exception('Failed to refresh token: ${response.body}');
|
120 |
+
}
|
121 |
+
} catch (e) {
|
122 |
+
throw Exception('Network error: $e');
|
123 |
+
}
|
124 |
+
}</code></pre>
|
125 |
+
|
126 |
+
<h4>Response:</h4>
|
127 |
+
<pre><code class="language-json">{
|
128 |
+
"access_token": "new-access-token",
|
129 |
+
"refresh_token": "your-refresh-token",
|
130 |
+
"token_type": "bearer",
|
131 |
+
"expires_in": 1800
|
132 |
+
}</code></pre>
|
133 |
+
</div>
|
134 |
+
|
135 |
+
<div class="endpoint" id="search">
|
136 |
+
<h3>3. Search Endpoint</h3>
|
137 |
+
<p><span class="method">POST</span><span class="endpoint-url">/search</span></p>
|
138 |
+
<p>This endpoint is used to send a search query and retrieve results. It requires a valid access token.</p>
|
139 |
+
|
140 |
+
<h4>cURL Request:</h4>
|
141 |
+
<pre><code class="language-bash">curl -X POST https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/search \
|
142 |
+
-H "Content-Type: application/json" \
|
143 |
+
-H "Authorization: Bearer your-access-token" \
|
144 |
+
-d '{"query": "test query"}'</code></pre>
|
145 |
+
|
146 |
+
<h4>Flutter Implementation:</h4>
|
147 |
+
<pre><code class="language-dart">class SearchResult {
|
148 |
+
final String text;
|
149 |
+
final double similarity;
|
150 |
+
final String modelType;
|
151 |
+
|
152 |
+
SearchResult({
|
153 |
+
required this.text,
|
154 |
+
required this.similarity,
|
155 |
+
required this.modelType,
|
156 |
+
});
|
157 |
+
|
158 |
+
factory SearchResult.fromJson(Map<String, dynamic> json) {
|
159 |
+
return SearchResult(
|
160 |
+
text: json['text'],
|
161 |
+
similarity: json['similarity'].toDouble(),
|
162 |
+
modelType: json['model_type'],
|
163 |
+
);
|
164 |
+
}
|
165 |
+
}
|
166 |
+
|
167 |
+
Future<List<SearchResult>> search(String query, String accessToken) async {
|
168 |
+
final url = Uri.parse('https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/search');
|
169 |
+
|
170 |
+
try {
|
171 |
+
final response = await http.post(
|
172 |
+
url,
|
173 |
+
headers: {
|
174 |
+
'Content-Type': 'application/json',
|
175 |
+
'Authorization': 'Bearer $accessToken',
|
176 |
+
},
|
177 |
+
body: jsonEncode({
|
178 |
+
'query': query,
|
179 |
+
}),
|
180 |
+
);
|
181 |
+
|
182 |
+
if (response.statusCode == 200) {
|
183 |
+
List<dynamic> jsonList = jsonDecode(response.body);
|
184 |
+
return jsonList.map((json) => SearchResult.fromJson(json)).toList();
|
185 |
+
} else {
|
186 |
+
throw Exception('Search failed: ${response.body}');
|
187 |
+
}
|
188 |
+
} catch (e) {
|
189 |
+
throw Exception('Network error: $e');
|
190 |
+
}
|
191 |
+
}</code></pre>
|
192 |
+
|
193 |
+
<h4>Response:</h4>
|
194 |
+
<pre><code class="language-json">[
|
195 |
+
{
|
196 |
+
"text": "Result 1 text",
|
197 |
+
"similarity": 0.95,
|
198 |
+
"model_type": "all-mpnet-base-v2"
|
199 |
+
},
|
200 |
+
{
|
201 |
+
"text": "Result 2 text",
|
202 |
+
"similarity": 0.92,
|
203 |
+
"model_type": "openai"
|
204 |
+
}
|
205 |
+
]</code></pre>
|
206 |
+
</div>
|
207 |
+
|
208 |
+
<div class="endpoint" id="save">
|
209 |
+
<h3>4. Save Data Endpoint</h3>
|
210 |
+
<p><span class="method">POST</span><span class="endpoint-url">/save</span></p>
|
211 |
+
<p>This endpoint is used to save user feedback and search results to the Hugging Face dataset. It requires a valid access token.</p>
|
212 |
+
|
213 |
+
<h4>cURL Request:</h4>
|
214 |
+
<pre><code class="language-bash">curl -X POST https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/save \
|
215 |
+
-H "Content-Type: application/json" \
|
216 |
+
-H "Authorization: Bearer your-access-token" \
|
217 |
+
-d '{
|
218 |
+
"items": [
|
219 |
+
{
|
220 |
+
"user_type": "user",
|
221 |
+
"username": "user123",
|
222 |
+
"query": "test query",
|
223 |
+
"retrieved_text": "Result 1 text",
|
224 |
+
"model_type": "all-mpnet-base-v2",
|
225 |
+
"reaction": "positive"
|
226 |
+
}
|
227 |
+
]
|
228 |
+
}'</code></pre>
|
229 |
+
|
230 |
+
<h4>Flutter Implementation:</h4>
|
231 |
+
<pre><code class="language-dart">class SaveInput {
|
232 |
+
final String userType;
|
233 |
+
final String username;
|
234 |
+
final String query;
|
235 |
+
final String retrievedText;
|
236 |
+
final String modelType;
|
237 |
+
final String reaction;
|
238 |
+
|
239 |
+
SaveInput({
|
240 |
+
required this.userType,
|
241 |
+
required this.username,
|
242 |
+
required this.query,
|
243 |
+
required this.retrievedText,
|
244 |
+
required this.modelType,
|
245 |
+
required this.reaction,
|
246 |
+
});
|
247 |
+
|
248 |
+
Map<String, dynamic> toJson() {
|
249 |
+
return {
|
250 |
+
'user_type': userType,
|
251 |
+
'username': username,
|
252 |
+
'query': query,
|
253 |
+
'retrieved_text': retrievedText,
|
254 |
+
'model_type': modelType,
|
255 |
+
'reaction': reaction,
|
256 |
+
};
|
257 |
+
}
|
258 |
+
}
|
259 |
+
|
260 |
+
Future<void> saveData(List<SaveInput> items, String accessToken) async {
|
261 |
+
final url = Uri.parse('https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/save');
|
262 |
+
|
263 |
+
try {
|
264 |
+
final response = await http.post(
|
265 |
+
url,
|
266 |
+
headers: {
|
267 |
+
'Content-Type': 'application/json',
|
268 |
+
'Authorization': 'Bearer $accessToken',
|
269 |
+
},
|
270 |
+
body: jsonEncode({
|
271 |
+
'items': items.map((item) => item.toJson()).toList(),
|
272 |
+
}),
|
273 |
+
);
|
274 |
+
|
275 |
+
if (response.statusCode != 200) {
|
276 |
+
throw Exception('Failed to save data: ${response.body}');
|
277 |
+
}
|
278 |
+
} catch (e) {
|
279 |
+
throw Exception('Network error: $e');
|
280 |
+
}
|
281 |
+
}</code></pre>
|
282 |
+
|
283 |
+
<h4>Response:</h4>
|
284 |
+
<pre><code class="language-json">{
|
285 |
+
"message": "Data saved successfully"
|
286 |
+
}</code></pre>
|
287 |
+
</div>
|
288 |
+
</section>
|
289 |
+
|
290 |
+
<section id="workflow">
|
291 |
+
<h2>Workflow Example</h2>
|
292 |
+
<p>Here's a complete workflow demonstrating how to use the API:</p>
|
293 |
+
|
294 |
+
<h3>cURL Implementation</h3>
|
295 |
+
<div class="endpoint">
|
296 |
+
<h3>Step 1: Login</h3>
|
297 |
+
<pre><code class="language-bash">curl -X POST https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/login \
|
298 |
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
299 |
+
-d 'username=user123&password=password123'</code></pre>
|
300 |
+
</div>
|
301 |
+
|
302 |
+
<div class="endpoint">
|
303 |
+
<h3>Step 2: Search</h3>
|
304 |
+
<pre><code class="language-bash">curl -X POST https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/search \
|
305 |
+
-H "Content-Type: application/json" \
|
306 |
+
-H "Authorization: Bearer your-access-token" \
|
307 |
+
-d '{"query": "test query"}'</code></pre>
|
308 |
+
</div>
|
309 |
+
|
310 |
+
<div class="endpoint">
|
311 |
+
<h3>Step 3: Save Data</h3>
|
312 |
+
<pre><code class="language-bash">curl -X POST https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/save \
|
313 |
+
-H "Content-Type: application/json" \
|
314 |
+
-H "Authorization: Bearer your-access-token" \
|
315 |
+
-d '{
|
316 |
+
"items": [
|
317 |
+
{
|
318 |
+
"user_type": "user",
|
319 |
+
"username": "user123",
|
320 |
+
"query": "test query",
|
321 |
+
"retrieved_text": "Result 1 text",
|
322 |
+
"model_type": "all-mpnet-base-v2",
|
323 |
+
"reaction": "positive"
|
324 |
+
}
|
325 |
+
]
|
326 |
+
}'</code></pre>
|
327 |
+
</div>
|
328 |
+
|
329 |
+
<div class="endpoint">
|
330 |
+
<h3>Step 4: Refresh Token</h3>
|
331 |
+
<pre><code class="language-bash">curl -X POST https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/refresh \
|
332 |
+
-H "Content-Type: application/json" \
|
333 |
+
-d '{"refresh_token": "your-refresh-token"}'</code></pre>
|
334 |
+
</div>
|
335 |
+
|
336 |
+
<h3>Flutter Implementation</h3>
|
337 |
+
<div class="endpoint">
|
338 |
+
<h4>Complete Flutter Example</h4>
|
339 |
+
<pre><code class="language-dart">import 'dart:convert';
|
340 |
+
import 'package:flutter/material.dart';
|
341 |
+
import 'package:http/http.dart' as http;
|
342 |
+
|
343 |
+
void main() {
|
344 |
+
runApp(MyApp());
|
345 |
+
}
|
346 |
+
|
347 |
+
class MyApp extends StatelessWidget {
|
348 |
+
@override
|
349 |
+
Widget build(BuildContext context) {
|
350 |
+
return MaterialApp(
|
351 |
+
title: 'Al Ghazali RAG API Example',
|
352 |
+
home: ApiWorkflowExample(),
|
353 |
+
);
|
354 |
+
}
|
355 |
+
}
|
356 |
+
|
357 |
+
class ApiWorkflowExample extends StatefulWidget {
|
358 |
+
@override
|
359 |
+
_ApiWorkflowExampleState createState() => _ApiWorkflowExampleState();
|
360 |
+
}
|
361 |
+
|
362 |
+
class _ApiWorkflowExampleState extends State<ApiWorkflowExample> {
|
363 |
+
String _accessToken = '';
|
364 |
+
String _refreshToken = '';
|
365 |
+
List<SearchResult> _searchResults = [];
|
366 |
+
|
367 |
+
Future<void> _login() async {
|
368 |
+
final username = 'user123';
|
369 |
+
final password = 'password123';
|
370 |
+
|
371 |
+
try {
|
372 |
+
final loginResponse = await login(username, password);
|
373 |
+
setState(() {
|
374 |
+
_accessToken = loginResponse.accessToken;
|
375 |
+
_refreshToken = loginResponse.refreshToken;
|
376 |
+
});
|
377 |
+
ScaffoldMessenger.of(context).showSnackBar(
|
378 |
+
SnackBar(content: Text('Login successful!')),
|
379 |
+
);
|
380 |
+
} catch (e) {
|
381 |
+
ScaffoldMessenger.of(context).showSnackBar(
|
382 |
+
SnackBar(content: Text('Login failed: $e')),
|
383 |
+
);
|
384 |
+
}
|
385 |
+
}
|
386 |
+
|
387 |
+
Future<void> _search() async {
|
388 |
+
final query = 'test query';
|
389 |
+
|
390 |
+
try {
|
391 |
+
final results = await search(query, _accessToken);
|
392 |
+
setState(() {
|
393 |
+
_searchResults = results;
|
394 |
+
});
|
395 |
+
ScaffoldMessenger.of(context).showSnackBar(
|
396 |
+
SnackBar(content: Text('Search successful!')),
|
397 |
+
);
|
398 |
+
} catch (e) {
|
399 |
+
ScaffoldMessenger.of(context).showSnackBar(
|
400 |
+
SnackBar(content: Text('Search failed: $e')),
|
401 |
+
);
|
402 |
+
}
|
403 |
+
}
|
404 |
+
|
405 |
+
Future<void> _saveData() async {
|
406 |
+
final items = [
|
407 |
+
SaveInput(
|
408 |
+
userType: 'user',
|
409 |
+
username: 'user123',
|
410 |
+
query: 'test query',
|
411 |
+
retrievedText: _searchResults[0].text,
|
412 |
+
modelType: _searchResults[0].modelType,
|
413 |
+
reaction: 'positive',
|
414 |
+
),
|
415 |
+
];
|
416 |
+
|
417 |
+
try {
|
418 |
+
await saveData(items, _accessToken);
|
419 |
+
ScaffoldMessenger.of(context).showSnackBar(
|
420 |
+
SnackBar(content: Text('Data saved successfully!')),
|
421 |
+
);
|
422 |
+
} catch (e) {
|
423 |
+
ScaffoldMessenger.of(context).showSnackBar(
|
424 |
+
SnackBar(content: Text('Failed to save data: $e')),
|
425 |
+
);
|
426 |
+
}
|
427 |
+
}
|
428 |
+
|
429 |
+
Future<void> _refreshToken() async {
|
430 |
+
try {
|
431 |
+
final loginResponse = await refreshToken(_refreshToken);
|
432 |
+
setState(() {
|
433 |
+
_accessToken = loginResponse.accessToken;
|
434 |
+
});
|
435 |
+
ScaffoldMessenger.of(context).showSnackBar(
|
436 |
+
SnackBar(content: Text('Token refreshed successfully!')),
|
437 |
+
);
|
438 |
+
} catch (e) {
|
439 |
+
ScaffoldMessenger.of(context).showSnackBar(
|
440 |
+
SnackBar(content: Text('Token refresh failed: $e')),
|
441 |
+
);
|
442 |
+
}
|
443 |
+
}
|
444 |
+
|
445 |
+
@override
|
446 |
+
Widget build(BuildContext context) {
|
447 |
+
return Scaffold(
|
448 |
+
appBar: AppBar(
|
449 |
+
title: Text('Al Ghazali RAG API Workflow'),
|
450 |
+
),
|
451 |
+
body: Padding(
|
452 |
+
padding: const EdgeInsets.all(16.0),
|
453 |
+
child: Column(
|
454 |
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
455 |
+
children: [
|
456 |
+
ElevatedButton(
|
457 |
+
onPressed: _login,
|
458 |
+
child: Text('Login'),
|
459 |
+
),
|
460 |
+
ElevatedButton(
|
461 |
+
onPressed: _search,
|
462 |
+
child: Text('Search'),
|
463 |
+
),
|
464 |
+
ElevatedButton(
|
465 |
+
onPressed: _saveData,
|
466 |
+
child: Text('Save Data'),
|
467 |
+
),
|
468 |
+
ElevatedButton(
|
469 |
+
onPressed: _refreshToken,
|
470 |
+
child: Text('Refresh Token'),
|
471 |
+
),
|
472 |
+
Expanded(
|
473 |
+
child: ListView.builder(
|
474 |
+
itemCount: _searchResults.length,
|
475 |
+
itemBuilder: (context, index) {
|
476 |
+
final result = _searchResults[index];
|
477 |
+
return ListTile(
|
478 |
+
title: Text(result.text),
|
479 |
+
subtitle: Text('Similarity: ${result.similarity}, Model: ${result.modelType}'),
|
480 |
+
);
|
481 |
+
},
|
482 |
+
),
|
483 |
+
),
|
484 |
+
],
|
485 |
+
),
|
486 |
+
),
|
487 |
+
);
|
488 |
+
}
|
489 |
+
}
|
490 |
+
|
491 |
+
Future<LoginResponse> login(String username, String password) async {
|
492 |
+
final url = Uri.parse('https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/login');
|
493 |
+
|
494 |
+
try {
|
495 |
+
final response = await http.post(
|
496 |
+
url,
|
497 |
+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
498 |
+
body: {
|
499 |
+
'username': username,
|
500 |
+
'password': password,
|
501 |
+
},
|
502 |
+
);
|
503 |
+
|
504 |
+
if (response.statusCode == 200) {
|
505 |
+
return LoginResponse.fromJson(jsonDecode(response.body));
|
506 |
+
} else {
|
507 |
+
throw Exception('Failed to login: ${response.body}');
|
508 |
+
}
|
509 |
+
} catch (e) {
|
510 |
+
throw Exception('Network error: $e');
|
511 |
+
}
|
512 |
+
}
|
513 |
+
|
514 |
+
Future<LoginResponse> refreshToken(String refreshToken) async {
|
515 |
+
final url = Uri.parse('https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/refresh');
|
516 |
+
|
517 |
+
try {
|
518 |
+
final response = await http.post(
|
519 |
+
url,
|
520 |
+
headers: {'Content-Type': 'application/json'},
|
521 |
+
body: jsonEncode({
|
522 |
+
'refresh_token': refreshToken,
|
523 |
+
}),
|
524 |
+
);
|
525 |
+
|
526 |
+
if (response.statusCode == 200) {
|
527 |
+
return LoginResponse.fromJson(jsonDecode(response.body));
|
528 |
+
} else {
|
529 |
+
throw Exception('Failed to refresh token: ${response.body}');
|
530 |
+
}
|
531 |
+
} catch (e) {
|
532 |
+
throw Exception('Network error: $e');
|
533 |
+
}
|
534 |
+
}
|
535 |
+
|
536 |
+
Future<List<SearchResult>> search(String query, String accessToken) async {
|
537 |
+
final url = Uri.parse('https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/search');
|
538 |
+
|
539 |
+
try {
|
540 |
+
final response = await http.post(
|
541 |
+
url,
|
542 |
+
headers: {
|
543 |
+
'Content-Type': 'application/json',
|
544 |
+
'Authorization': 'Bearer $accessToken',
|
545 |
+
},
|
546 |
+
body: jsonEncode({
|
547 |
+
'query': query,
|
548 |
+
}),
|
549 |
+
);
|
550 |
+
|
551 |
+
if (response.statusCode == 200) {
|
552 |
+
List<dynamic> jsonList = jsonDecode(response.body);
|
553 |
+
return jsonList.map((json) => SearchResult.fromJson(json)).toList();
|
554 |
+
} else {
|
555 |
+
throw Exception('Search failed: ${response.body}');
|
556 |
+
}
|
557 |
+
} catch (e) {
|
558 |
+
throw Exception('Network error: $e');
|
559 |
+
}
|
560 |
+
}
|
561 |
+
|
562 |
+
Future<void> saveData(List<SaveInput> items, String accessToken) async {
|
563 |
+
final url = Uri.parse('https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/save');
|
564 |
+
|
565 |
+
try {
|
566 |
+
final response = await http.post(
|
567 |
+
url,
|
568 |
+
headers: {
|
569 |
+
'Content-Type': 'application/json',
|
570 |
+
'Authorization': 'Bearer $accessToken',
|
571 |
+
},
|
572 |
+
body: jsonEncode({
|
573 |
+
'items': items.map((item) => item.toJson()).toList(),
|
574 |
+
}),
|
575 |
+
);
|
576 |
+
|
577 |
+
if (response.statusCode != 200) {
|
578 |
+
throw Exception('Failed to save data: ${response.body}');
|
579 |
+
}
|
580 |
+
} catch (e) {
|
581 |
+
throw Exception('Network error: $e');
|
582 |
+
}
|
583 |
+
}
|
584 |
+
|
585 |
+
class LoginResponse {
|
586 |
+
final String accessToken;
|
587 |
+
final String refreshToken;
|
588 |
+
final String tokenType;
|
589 |
+
final int expiresIn;
|
590 |
+
|
591 |
+
LoginResponse({
|
592 |
+
required this.accessToken,
|
593 |
+
required this.refreshToken,
|
594 |
+
required this.tokenType,
|
595 |
+
required this.expiresIn,
|
596 |
+
});
|
597 |
+
|
598 |
+
factory LoginResponse.fromJson(Map<String, dynamic> json) {
|
599 |
+
return LoginResponse(
|
600 |
+
accessToken: json['access_token'],
|
601 |
+
refreshToken: json['refresh_token'],
|
602 |
+
tokenType: json['token_type'],
|
603 |
+
expiresIn: json['expires_in'],
|
604 |
+
);
|
605 |
+
}
|
606 |
+
}
|
607 |
+
|
608 |
+
class SearchResult {
|
609 |
+
final String text;
|
610 |
+
final double similarity;
|
611 |
+
final String modelType;
|
612 |
+
|
613 |
+
SearchResult({
|
614 |
+
required this.text,
|
615 |
+
required this.similarity,
|
616 |
+
required this.modelType,
|
617 |
+
});
|
618 |
+
|
619 |
+
factory SearchResult.fromJson(Map<String, dynamic> json) {
|
620 |
+
return SearchResult(
|
621 |
+
text: json['text'],
|
622 |
+
similarity: json['similarity'].toDouble(),
|
623 |
+
modelType: json['model_type'],
|
624 |
+
);
|
625 |
+
}
|
626 |
+
}
|
627 |
+
|
628 |
+
class SaveInput {
|
629 |
+
final String userType;
|
630 |
+
final String username;
|
631 |
+
final String query;
|
632 |
+
final String retrievedText;
|
633 |
+
final String modelType;
|
634 |
+
final String reaction;
|
635 |
+
|
636 |
+
SaveInput({
|
637 |
+
required this.userType,
|
638 |
+
required this.username,
|
639 |
+
required this.query,
|
640 |
+
required this.retrievedText,
|
641 |
+
required this.modelType,
|
642 |
+
required this.reaction,
|
643 |
+
});
|
644 |
+
|
645 |
+
Map<String, dynamic> toJson() {
|
646 |
+
return {
|
647 |
+
'user_type': userType,
|
648 |
+
'username': username,
|
649 |
+
'query': query,
|
650 |
+
'retrieved_text': retrievedText,
|
651 |
+
'model_type': modelType,
|
652 |
+
'reaction': reaction,
|
653 |
+
};
|
654 |
+
}
|
655 |
+
}</code></pre>
|
656 |
+
</div>
|
657 |
+
</section>
|
658 |
+
</main>
|
659 |
+
</div>
|
660 |
+
|
661 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prismjs/1.29.0/prism.min.js"></script>
|
662 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prismjs/1.29.0/components/prism-bash.min.js"></script>
|
663 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prismjs/1.29.0/components/prism-json.min.js"></script>
|
664 |
+
<script src="/home/script.js"></script>
|
665 |
+
</body>
|
666 |
+
</html>
|
static/script.js
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Generate navigation items
|
2 |
+
const sections = [
|
3 |
+
{ id: 'endpoints', title: 'Endpoints' },
|
4 |
+
{ id: 'login', title: 'Login' },
|
5 |
+
{ id: 'refresh', title: 'Refresh Token' },
|
6 |
+
{ id: 'search', title: 'Search' },
|
7 |
+
{ id: 'save', title: 'Save'},
|
8 |
+
{ id: 'workflow', title: 'Workflow Example' }
|
9 |
+
];
|
10 |
+
|
11 |
+
const navItems = document.querySelector('.nav-items');
|
12 |
+
sections.forEach(section => {
|
13 |
+
const item = document.createElement('div');
|
14 |
+
item.className = 'nav-item';
|
15 |
+
item.textContent = section.title;
|
16 |
+
item.setAttribute('data-section', section.id);
|
17 |
+
item.addEventListener('click', () => {
|
18 |
+
document.getElementById(section.id).scrollIntoView({ behavior: 'smooth' });
|
19 |
+
});
|
20 |
+
navItems.appendChild(item);
|
21 |
+
});
|
22 |
+
|
23 |
+
// Highlight current section in navigation
|
24 |
+
const observerCallback = (entries) => {
|
25 |
+
entries.forEach(entry => {
|
26 |
+
if (entry.isIntersecting) {
|
27 |
+
const currentNav = document.querySelector(`.nav-item[data-section="${entry.target.id}"]`);
|
28 |
+
if (currentNav) {
|
29 |
+
document.querySelectorAll('.nav-item').forEach(item => {
|
30 |
+
item.style.backgroundColor = '';
|
31 |
+
});
|
32 |
+
currentNav.style.backgroundColor = '#f3f4f6';
|
33 |
+
}
|
34 |
+
}
|
35 |
+
});
|
36 |
+
};
|
37 |
+
|
38 |
+
const observer = new IntersectionObserver(observerCallback, {
|
39 |
+
threshold: 0.2
|
40 |
+
});
|
41 |
+
|
42 |
+
document.querySelectorAll('section').forEach(section => {
|
43 |
+
observer.observe(section);
|
44 |
+
});
|
static/style.css
ADDED
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
:root {
|
2 |
+
--primary-color: #4f46e5;
|
3 |
+
--secondary-color: #1e40af;
|
4 |
+
--text-color: #1f2937;
|
5 |
+
--code-bg: #1e1e1e;
|
6 |
+
--border-color: #e5e7eb;
|
7 |
+
}
|
8 |
+
|
9 |
+
* {
|
10 |
+
margin: 0;
|
11 |
+
padding: 0;
|
12 |
+
box-sizing: border-box;
|
13 |
+
}
|
14 |
+
|
15 |
+
body {
|
16 |
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
17 |
+
line-height: 1.6;
|
18 |
+
color: var(--text-color);
|
19 |
+
background-color: #f9fafb;
|
20 |
+
}
|
21 |
+
|
22 |
+
.container {
|
23 |
+
display: flex;
|
24 |
+
min-height: 100vh;
|
25 |
+
}
|
26 |
+
|
27 |
+
.sidebar {
|
28 |
+
width: 280px;
|
29 |
+
background-color: white;
|
30 |
+
border-right: 1px solid var(--border-color);
|
31 |
+
padding: 2rem;
|
32 |
+
position: fixed;
|
33 |
+
height: 100vh;
|
34 |
+
overflow-y: auto;
|
35 |
+
}
|
36 |
+
|
37 |
+
.content {
|
38 |
+
flex: 1;
|
39 |
+
padding: 2rem;
|
40 |
+
margin-left: 280px;
|
41 |
+
max-width: 1200px;
|
42 |
+
}
|
43 |
+
|
44 |
+
.logo {
|
45 |
+
font-size: 1.5rem;
|
46 |
+
font-weight: bold;
|
47 |
+
color: var(--primary-color);
|
48 |
+
margin-bottom: 2rem;
|
49 |
+
display: flex;
|
50 |
+
align-items: center;
|
51 |
+
gap: 0.5rem;
|
52 |
+
}
|
53 |
+
|
54 |
+
.nav-item {
|
55 |
+
margin: 0.5rem 0;
|
56 |
+
cursor: pointer;
|
57 |
+
padding: 0.5rem;
|
58 |
+
border-radius: 0.375rem;
|
59 |
+
transition: background-color 0.2s;
|
60 |
+
}
|
61 |
+
|
62 |
+
.nav-item:hover {
|
63 |
+
background-color: #f3f4f6;
|
64 |
+
}
|
65 |
+
|
66 |
+
h1 {
|
67 |
+
font-size: 2.5rem;
|
68 |
+
margin-bottom: 1.5rem;
|
69 |
+
color: var(--primary-color);
|
70 |
+
}
|
71 |
+
|
72 |
+
h2 {
|
73 |
+
font-size: 1.8rem;
|
74 |
+
margin: 2rem 0 1rem;
|
75 |
+
padding-top: 2rem;
|
76 |
+
border-top: 1px solid var(--border-color);
|
77 |
+
}
|
78 |
+
|
79 |
+
h3 {
|
80 |
+
font-size: 1.4rem;
|
81 |
+
margin: 1.5rem 0 1rem;
|
82 |
+
color: var(--secondary-color);
|
83 |
+
}
|
84 |
+
|
85 |
+
p {
|
86 |
+
margin-bottom: 1rem;
|
87 |
+
}
|
88 |
+
|
89 |
+
pre {
|
90 |
+
margin: 1rem 0;
|
91 |
+
padding: 1rem;
|
92 |
+
border-radius: 0.5rem;
|
93 |
+
background-color: #2d2d2d !important;
|
94 |
+
overflow-x: auto;
|
95 |
+
border: 1px solid #3f3f3f;
|
96 |
+
}
|
97 |
+
|
98 |
+
code {
|
99 |
+
font-family: 'Fira Code', monospace;
|
100 |
+
font-size: 0.9rem;
|
101 |
+
color: #e6e6e6;
|
102 |
+
}
|
103 |
+
|
104 |
+
/* Specific syntax highlighting overrides */
|
105 |
+
.language-bash {
|
106 |
+
color: #e6e6e6 !important;
|
107 |
+
}
|
108 |
+
|
109 |
+
.language-bash .token.function {
|
110 |
+
color: #87ceeb !important;
|
111 |
+
}
|
112 |
+
|
113 |
+
.language-bash .token.operator {
|
114 |
+
color: #87ceeb !important;
|
115 |
+
}
|
116 |
+
|
117 |
+
.language-bash .token.string {
|
118 |
+
color: #98c379 !important;
|
119 |
+
}
|
120 |
+
|
121 |
+
.language-json {
|
122 |
+
color: #e6e6e6 !important;
|
123 |
+
}
|
124 |
+
|
125 |
+
.language-json .token.property {
|
126 |
+
color: #87ceeb !important;
|
127 |
+
}
|
128 |
+
|
129 |
+
.language-json .token.string {
|
130 |
+
color: #98c379 !important;
|
131 |
+
}
|
132 |
+
|
133 |
+
.language-json .token.number {
|
134 |
+
color: #d19a66 !important;
|
135 |
+
}
|
136 |
+
|
137 |
+
/* Add a subtle glow effect to code blocks */
|
138 |
+
pre code {
|
139 |
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
140 |
+
}
|
141 |
+
|
142 |
+
.endpoint {
|
143 |
+
background-color: white;
|
144 |
+
border: 1px solid var(--border-color);
|
145 |
+
border-radius: 0.5rem;
|
146 |
+
padding: 1.5rem;
|
147 |
+
margin: 1.5rem 0;
|
148 |
+
}
|
149 |
+
|
150 |
+
.method {
|
151 |
+
display: inline-block;
|
152 |
+
padding: 0.25rem 0.5rem;
|
153 |
+
border-radius: 0.25rem;
|
154 |
+
background-color: var(--primary-color);
|
155 |
+
color: white;
|
156 |
+
font-weight: 500;
|
157 |
+
margin-right: 0.5rem;
|
158 |
+
}
|
159 |
+
|
160 |
+
.endpoint-url {
|
161 |
+
font-family: 'Fira Code', monospace;
|
162 |
+
color: var(--secondary-color);
|
163 |
+
}
|
164 |
+
|
165 |
+
@media (max-width: 768px) {
|
166 |
+
.sidebar {
|
167 |
+
display: none;
|
168 |
+
}
|
169 |
+
.content {
|
170 |
+
margin-left: 0;
|
171 |
+
}
|
172 |
+
}
|
utils.py
ADDED
File without changes
|