File size: 7,907 Bytes
8d60e33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# app/main.py
from __future__ import annotations

import logging
import os
import time
from contextlib import asynccontextmanager
from typing import Any

from fastapi import FastAPI, APIRouter, Request
from fastapi.responses import RedirectResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

# ---- Early env load (HF_TOKEN, ADMIN_TOKEN, GITHUB_TOKEN, etc.) ----
def _load_env_file(paths: list[str]) -> None:
    logger = logging.getLogger("uvicorn.error")
    try:
        from dotenv import load_dotenv  # type: ignore
        for p in paths:
            if os.path.exists(p):
                load_dotenv(dotenv_path=p, override=False)
                logger.info("Loaded environment from %s", p)
                return
        logger.info("No .env file found in %s (skipping)", paths)
    except Exception:
        # Fallback, very small .env parser
        for p in paths:
            if not os.path.exists(p):
                continue
            try:
                with open(p, "r", encoding="utf-8") as f:
                    for raw in f:
                        line = raw.strip()
                        if not line or line.startswith("#"):
                            continue
                        if line.startswith("export "):
                            line = line[len("export "):].strip()
                        if "=" not in line:
                            continue
                        key, val = line.split("=", 1)
                        key, val = key.strip(), val.strip()
                        if (val.startswith('"') and val.endswith('"')) or (
                            val.startswith("'") and val.endswith("'")
                        ):
                            val = val[1:-1]
                        os.environ.setdefault(key, val)
                logger.info("Loaded environment from %s (fallback parser)", p)
                return
            except Exception as e:
                logger.warning("Failed loading env from %s: %s", p, e)
        logger.info("No .env loaded (none found / parsers failed)")

_load_env_file([".env", "configs/.env", ".env.local", "configs/.env.local"])

# ---- RAG DISABLED (commented out while debugging) ----
# from .deps import get_settings
# from .services.chat_service import get_retriever
# from .core.rag.build import ensure_kb

# ---- Middlewares ----
try:
    from .middleware import attach_middlewares  # type: ignore
except Exception:
    try:
        from .middlewares import attach_middlewares  # type: ignore
    except Exception:
        def attach_middlewares(app: FastAPI) -> None:
            logging.getLogger("uvicorn.error").warning(
                "attach_middlewares not found; continuing without custom middlewares."
            )

# ---- Routers enabled ----
from .routers import health
from .ui import router as ui_router  # <-- mount UI so /home works

# ---- Validator service integration ----
VALIDATOR_TAG = {"name": "Validator", "description": "A2A Validator UI and endpoints (/validator)."}

HAS_VALIDATOR = False
HAS_SOCKETIO = False
socketio_app = None  # type: ignore[assignment]

try:
    # Primary validator router + optional Socket.IO app
    from .services.validator_service import router as validator_router  # type: ignore
    HAS_VALIDATOR = True
    try:
        from .services.validator_service import socketio_app as _socketio_app  # type: ignore
        socketio_app = _socketio_app
        HAS_SOCKETIO = socketio_app is not None
    except Exception:
        socketio_app = None
        HAS_SOCKETIO = False
except Exception as e:
    logging.getLogger("uvicorn.error").warning("validator_service import failed: %s", e)
    # Fallback validator router if import fails
    _templates = Jinja2Templates(directory="app/templates")
    validator_router = APIRouter(prefix="/validator", tags=["Validator"])

    @validator_router.get("", response_class=HTMLResponse)
    @validator_router.get("/", response_class=HTMLResponse)
    async def _validator_fallback_ui(request: Request) -> HTMLResponse:
        # Try validator.hml first (project used this name), then validator.html
        try:
            return _templates.TemplateResponse("validator.hml", {"request": request})
        except Exception:
            return _templates.TemplateResponse(
                "validator.html",
                {"request": request, "warning": "validator service running in fallback mode"},
            )

TAGS_METADATA = [
    {"name": "Health", "description": "Liveness / readiness probes and basic service metadata."},
    VALIDATOR_TAG,
    # UI tag is implicit; only /home (Info) and /validator are exposed
]

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.started_at = time.time()
    app.state.version = os.getenv("APP_VERSION", "1.0.0")
    logger = logging.getLogger("uvicorn.error")

    # ---- RAG INIT DISABLED ----
    # try:
    #     if ensure_kb(out_jsonl="data/kb.jsonl", config_path="configs/rag_sources.yaml", skip_if_exists=True):
    #         logger.info("KB ready at data/kb.jsonl")
    #     else:
    #         logger.warning("KB build produced no records; running LLM-only.")
    # except Exception as e:
    #     logger.warning("KB build failed (%s); running LLM-only.", e)
    # logger.info("Warming up RAG retriever...")
    # get_retriever(get_settings())
    # logger.info("RAG retriever is ready.")

    hf_token_present = bool(os.getenv("HF_TOKEN"))
    logger.info(
        "matrix-ai starting (version=%s, port=%s, hf_token_present=%s)",
        app.state.version,
        os.getenv("PORT", "7860"),
        "yes" if hf_token_present else "no",
    )
    try:
        yield
    finally:
        uptime = time.time() - getattr(app.state, "started_at", time.time())
        logger.info("matrix-ai shutting down (uptime=%.2fs)", uptime)

def create_app() -> FastAPI:
    app = FastAPI(
        title="matrix-ai",
        version=os.getenv("APP_VERSION", "1.0.0"),
        description="Minimal service with A2A Validator and health endpoints",
        openapi_tags=TAGS_METADATA,
        docs_url="/docs",
        redoc_url=None,
        lifespan=lifespan,
    )

    # Static files (for validator UI assets, etc.)
    try:
        app.mount("/static", StaticFiles(directory="app/static"), name="static")
    except Exception:
        pass

    # Middlewares (gzip, CORS, rate-limit, req-logs, etc.)
    attach_middlewares(app)

    # Core info/router pages
    app.include_router(health.router, tags=["Health"])

    # Validator router
    app.include_router(validator_router, tags=["Validator"])

    # UI router (enables /home "Info" page and "/" redirect defined in ui.py)
    app.include_router(ui_router)

    # Alias so the frontend can POST /agent-card (script.js default target)
    try:
        from .services.validator_service import get_agent_card as _get_agent_card  # type: ignore
        app.add_api_route(
            "/agent-card",
            _get_agent_card,
            methods=["POST"],
            tags=["Validator"],
            name="agent_card_alias",
        )
        logging.getLogger("uvicorn.error").info(
            "Added alias: POST /agent-card → /validator/agent-card"
        )
    except Exception as e:
        logging.getLogger("uvicorn.error").warning(
            f"Failed to add /agent-card alias: {e}"
        )

    # Mount Socket.IO if available
    if HAS_SOCKETIO and socketio_app is not None:
        app.mount("/socket.io", socketio_app)
        logging.getLogger("uvicorn.error").info("Mounted Socket.IO at /socket.io")

    # IMPORTANT:
    # Do NOT define extra "/" or "/home" handlers here.
    # ui.py already defines:
    #   - GET "/"  -> Redirect to /validator
    #   - GET "/home" -> Render home.html (Info tab)
    # Keeping only one definition avoids duplicate-route conflicts.

    return app

app = create_app()