Read from .env
Browse files- app/main.py +70 -3
- app/routers/chat.py +51 -11
- app/services/chat_service.py +21 -8
- app/templates/base.html +2 -0
app/main.py
CHANGED
|
@@ -9,7 +9,62 @@ from typing import Any, Dict
|
|
| 9 |
from fastapi import FastAPI
|
| 10 |
from fastapi.responses import RedirectResponse
|
| 11 |
|
| 12 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
# Prefer the canonical package name; if your repo uses "middlewares/", this tries both.
|
| 14 |
try:
|
| 15 |
from .middleware import attach_middlewares # singular
|
|
@@ -22,7 +77,9 @@ except Exception:
|
|
| 22 |
"attach_middlewares not found; continuing without custom middlewares."
|
| 23 |
)
|
| 24 |
|
| 25 |
-
#
|
|
|
|
|
|
|
| 26 |
from .routers import health, plan, chat
|
| 27 |
|
| 28 |
# Optional UI (Home/Chat/Dev). If missing, we gracefully fall back to a JSON root.
|
|
@@ -32,6 +89,7 @@ try:
|
|
| 32 |
except Exception: # pragma: no cover
|
| 33 |
HAS_UI = False
|
| 34 |
|
|
|
|
| 35 |
TAGS_METADATA = [
|
| 36 |
{"name": "Health", "description": "Liveness / readiness probes and basic service metadata."},
|
| 37 |
{"name": "Planning", "description": "AI plan generation for Matrix Guardian (/v1/plan)."},
|
|
@@ -39,12 +97,19 @@ TAGS_METADATA = [
|
|
| 39 |
{"name": "UI", "description": "Minimal web UI (Home, Chat, Dev) if enabled."},
|
| 40 |
]
|
| 41 |
|
|
|
|
| 42 |
@asynccontextmanager
|
| 43 |
async def lifespan(app: FastAPI):
|
| 44 |
app.state.started_at = time.time()
|
| 45 |
app.state.version = os.getenv("APP_VERSION", "1.0.0")
|
|
|
|
|
|
|
|
|
|
| 46 |
logging.getLogger("uvicorn.error").info(
|
| 47 |
-
"matrix-ai starting (version=%s, port=%s
|
|
|
|
|
|
|
|
|
|
| 48 |
)
|
| 49 |
try:
|
| 50 |
yield
|
|
@@ -54,6 +119,7 @@ async def lifespan(app: FastAPI):
|
|
| 54 |
"matrix-ai shutting down (uptime=%.2fs)", uptime
|
| 55 |
)
|
| 56 |
|
|
|
|
| 57 |
def create_app() -> FastAPI:
|
| 58 |
app = FastAPI(
|
| 59 |
title="matrix-ai",
|
|
@@ -94,4 +160,5 @@ def create_app() -> FastAPI:
|
|
| 94 |
|
| 95 |
return app
|
| 96 |
|
|
|
|
| 97 |
app = create_app()
|
|
|
|
| 9 |
from fastapi import FastAPI
|
| 10 |
from fastapi.responses import RedirectResponse
|
| 11 |
|
| 12 |
+
# -----------------------------------------------------------------------------
|
| 13 |
+
# Early: load .env (so HF_TOKEN, ADMIN_TOKEN, etc. are available locally)
|
| 14 |
+
# -----------------------------------------------------------------------------
|
| 15 |
+
def _load_env_file(paths: list[str]) -> None:
|
| 16 |
+
"""Load environment variables from the first existing path in `paths`.
|
| 17 |
+
Prefer python-dotenv if present; otherwise use a tiny fallback parser."""
|
| 18 |
+
logger = logging.getLogger("uvicorn.error")
|
| 19 |
+
|
| 20 |
+
# 1) Try python-dotenv (best)
|
| 21 |
+
try:
|
| 22 |
+
from dotenv import load_dotenv # type: ignore
|
| 23 |
+
for p in paths:
|
| 24 |
+
if os.path.exists(p):
|
| 25 |
+
load_dotenv(dotenv_path=p, override=False)
|
| 26 |
+
logger.info("Loaded environment from %s", p)
|
| 27 |
+
return
|
| 28 |
+
logger.info("No .env file found in %s (skipping)", paths)
|
| 29 |
+
return
|
| 30 |
+
except Exception:
|
| 31 |
+
# 2) Fallback: simple parser
|
| 32 |
+
for p in paths:
|
| 33 |
+
if not os.path.exists(p):
|
| 34 |
+
continue
|
| 35 |
+
try:
|
| 36 |
+
with open(p, "r", encoding="utf-8") as f:
|
| 37 |
+
for raw in f:
|
| 38 |
+
line = raw.strip()
|
| 39 |
+
if not line or line.startswith("#"):
|
| 40 |
+
continue
|
| 41 |
+
if line.startswith("export "):
|
| 42 |
+
line = line[len("export ") :].strip()
|
| 43 |
+
if "=" not in line:
|
| 44 |
+
continue
|
| 45 |
+
key, val = line.split("=", 1)
|
| 46 |
+
key, val = key.strip(), val.strip()
|
| 47 |
+
# strip optional quotes
|
| 48 |
+
if (val.startswith('"') and val.endswith('"')) or (
|
| 49 |
+
val.startswith("'") and val.endswith("'")
|
| 50 |
+
):
|
| 51 |
+
val = val[1:-1]
|
| 52 |
+
# do not clobber existing env (Space Secrets)
|
| 53 |
+
os.environ.setdefault(key, val)
|
| 54 |
+
logger.info("Loaded environment from %s (fallback parser)", p)
|
| 55 |
+
return
|
| 56 |
+
except Exception as e:
|
| 57 |
+
logger.warning("Failed loading env from %s: %s", p, e)
|
| 58 |
+
|
| 59 |
+
logger.info("No .env loaded (none found / parsers failed)")
|
| 60 |
+
|
| 61 |
+
# Try typical locations for local dev. HF Spaces will ignore this and use Secrets.
|
| 62 |
+
_load_env_file([".env", "configs/.env", ".env.local", "configs/.env.local"])
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# -----------------------------------------------------------------------------
|
| 66 |
+
# Middlewares
|
| 67 |
+
# -----------------------------------------------------------------------------
|
| 68 |
# Prefer the canonical package name; if your repo uses "middlewares/", this tries both.
|
| 69 |
try:
|
| 70 |
from .middleware import attach_middlewares # singular
|
|
|
|
| 77 |
"attach_middlewares not found; continuing without custom middlewares."
|
| 78 |
)
|
| 79 |
|
| 80 |
+
# -----------------------------------------------------------------------------
|
| 81 |
+
# Routers
|
| 82 |
+
# -----------------------------------------------------------------------------
|
| 83 |
from .routers import health, plan, chat
|
| 84 |
|
| 85 |
# Optional UI (Home/Chat/Dev). If missing, we gracefully fall back to a JSON root.
|
|
|
|
| 89 |
except Exception: # pragma: no cover
|
| 90 |
HAS_UI = False
|
| 91 |
|
| 92 |
+
|
| 93 |
TAGS_METADATA = [
|
| 94 |
{"name": "Health", "description": "Liveness / readiness probes and basic service metadata."},
|
| 95 |
{"name": "Planning", "description": "AI plan generation for Matrix Guardian (/v1/plan)."},
|
|
|
|
| 97 |
{"name": "UI", "description": "Minimal web UI (Home, Chat, Dev) if enabled."},
|
| 98 |
]
|
| 99 |
|
| 100 |
+
|
| 101 |
@asynccontextmanager
|
| 102 |
async def lifespan(app: FastAPI):
|
| 103 |
app.state.started_at = time.time()
|
| 104 |
app.state.version = os.getenv("APP_VERSION", "1.0.0")
|
| 105 |
+
|
| 106 |
+
# Minimal diagnostics; HF_TOKEN presence matters for inference
|
| 107 |
+
hf_token_present = bool(os.getenv("HF_TOKEN"))
|
| 108 |
logging.getLogger("uvicorn.error").info(
|
| 109 |
+
"matrix-ai starting (version=%s, port=%s, hf_token_present=%s)",
|
| 110 |
+
app.state.version,
|
| 111 |
+
os.getenv("PORT", "7860"),
|
| 112 |
+
"yes" if hf_token_present else "no",
|
| 113 |
)
|
| 114 |
try:
|
| 115 |
yield
|
|
|
|
| 119 |
"matrix-ai shutting down (uptime=%.2fs)", uptime
|
| 120 |
)
|
| 121 |
|
| 122 |
+
|
| 123 |
def create_app() -> FastAPI:
|
| 124 |
app = FastAPI(
|
| 125 |
title="matrix-ai",
|
|
|
|
| 160 |
|
| 161 |
return app
|
| 162 |
|
| 163 |
+
|
| 164 |
app = create_app()
|
app/routers/chat.py
CHANGED
|
@@ -1,18 +1,58 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
| 2 |
from ..deps import get_settings
|
| 3 |
from ..core.config import Settings
|
| 4 |
-
from ..
|
| 5 |
-
from ..services.chat_service import chat_answer
|
| 6 |
|
| 7 |
router = APIRouter()
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
@router.post("/chat", response_model=ChatResponse)
|
| 10 |
-
async def
|
| 11 |
-
req: ChatRequest,
|
| 12 |
-
settings: Settings = Depends(get_settings)
|
| 13 |
-
):
|
| 14 |
-
"""Answers questions about the MatrixHub ecosystem using RAG."""
|
| 15 |
try:
|
| 16 |
-
|
| 17 |
-
except
|
| 18 |
-
raise HTTPException(status_code=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/routers/chat.py
|
| 2 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from typing import List, Optional
|
| 5 |
from ..deps import get_settings
|
| 6 |
from ..core.config import Settings
|
| 7 |
+
from ..services.chat_service import ChatService
|
|
|
|
| 8 |
|
| 9 |
router = APIRouter()
|
| 10 |
|
| 11 |
+
class ChatMessage(BaseModel):
|
| 12 |
+
role: str
|
| 13 |
+
content: str
|
| 14 |
+
|
| 15 |
+
class ChatRequest(BaseModel):
|
| 16 |
+
# accept several shapes so UI/clients don't 422:
|
| 17 |
+
query: Optional[str] = None
|
| 18 |
+
question: Optional[str] = None
|
| 19 |
+
prompt: Optional[str] = None
|
| 20 |
+
messages: Optional[List[ChatMessage]] = None
|
| 21 |
+
|
| 22 |
+
def as_text(self) -> str:
|
| 23 |
+
if self.query:
|
| 24 |
+
return self.query
|
| 25 |
+
if self.question:
|
| 26 |
+
return self.question
|
| 27 |
+
if self.prompt:
|
| 28 |
+
return self.prompt
|
| 29 |
+
if self.messages:
|
| 30 |
+
# prefer last user message
|
| 31 |
+
for m in reversed(self.messages):
|
| 32 |
+
if m.role.lower() == "user":
|
| 33 |
+
return m.content
|
| 34 |
+
# fallback to last message
|
| 35 |
+
if self.messages:
|
| 36 |
+
return self.messages[-1].content
|
| 37 |
+
raise ValueError("Body must include 'query'/'question'/'prompt' or 'messages'")
|
| 38 |
+
|
| 39 |
+
class ChatResponse(BaseModel):
|
| 40 |
+
answer: str
|
| 41 |
+
|
| 42 |
@router.post("/chat", response_model=ChatResponse)
|
| 43 |
+
async def chat(req: ChatRequest, settings: Settings = Depends(get_settings)):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
try:
|
| 45 |
+
text = req.as_text()
|
| 46 |
+
except ValueError as e:
|
| 47 |
+
raise HTTPException(status_code=422, detail=str(e))
|
| 48 |
+
svc = ChatService(settings)
|
| 49 |
+
answer = await svc.answer(text)
|
| 50 |
+
return ChatResponse(answer=answer)
|
| 51 |
+
|
| 52 |
+
# Handy for curl/browser tests:
|
| 53 |
+
# GET /v1/chat?query=hello
|
| 54 |
+
@router.get("/chat", response_model=ChatResponse)
|
| 55 |
+
async def chat_get(query: str = Query(...), settings: Settings = Depends(get_settings)):
|
| 56 |
+
svc = ChatService(settings)
|
| 57 |
+
answer = await svc.answer(query)
|
| 58 |
+
return ChatResponse(answer=answer)
|
app/services/chat_service.py
CHANGED
|
@@ -1,10 +1,23 @@
|
|
| 1 |
-
#
|
| 2 |
-
from
|
| 3 |
from ..core.config import Settings
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
"
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/services/chat_service.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
from ..core.config import Settings
|
| 4 |
+
from ..core.inference.client import HFClient
|
| 5 |
|
| 6 |
+
SYSTEM_PROMPT = (
|
| 7 |
+
"You are MATRIX-AI, a concise, helpful assistant for the Matrix EcoSystem. "
|
| 8 |
+
"Answer clearly and briefly. If unsure, say so."
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
class ChatService:
|
| 12 |
+
def __init__(self, settings: Settings):
|
| 13 |
+
self.settings = settings
|
| 14 |
+
self.client = HFClient(model=settings.model.name)
|
| 15 |
+
|
| 16 |
+
async def answer(self, query: str) -> str:
|
| 17 |
+
prompt = f"{SYSTEM_PROMPT}\n\nUser: {query}\nAssistant:"
|
| 18 |
+
text = await self.client.generate(
|
| 19 |
+
prompt=prompt,
|
| 20 |
+
max_new_tokens=self.settings.model.max_new_tokens,
|
| 21 |
+
temperature=self.settings.model.temperature,
|
| 22 |
+
)
|
| 23 |
+
return (text or "").strip()
|
app/templates/base.html
CHANGED
|
@@ -156,6 +156,8 @@
|
|
| 156 |
<a href="/chat">Chat</a>
|
| 157 |
<a href="/dev">Dev</a>
|
| 158 |
<a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
|
|
|
|
|
|
|
| 159 |
</nav>
|
| 160 |
</header>
|
| 161 |
|
|
|
|
| 156 |
<a href="/chat">Chat</a>
|
| 157 |
<a href="/dev">Dev</a>
|
| 158 |
<a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
|
| 159 |
+
<a href="https://github.com/agent-matrix/matrix-ai" target="_blank" rel="noreferrer" title="Give me a star on GitHub!">GitHub</a>
|
| 160 |
+
|
| 161 |
</nav>
|
| 162 |
</header>
|
| 163 |
|