gragdb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- grag/__init__.py +8 -0
- grag/api/__init__.py +1 -0
- grag/api/main.py +204 -0
- grag/api/static/assets/index-C2RYf8Fw.js +94 -0
- grag/api/static/assets/index-Cqmq96EV.css +1 -0
- grag/api/static/index.html +13 -0
- grag/cli.py +88 -0
- grag/config.py +58 -0
- grag/core/__init__.py +1 -0
- grag/core/engine.py +344 -0
- grag/core/errors.py +39 -0
- grag/core/mutate.py +609 -0
- grag/core/schema.py +223 -0
- grag/core/serialize.py +126 -0
- grag/core/types.py +318 -0
- grag/ingest/__init__.py +1 -0
- grag/ingest/loaders.py +292 -0
- grag/mcp_server/__init__.py +1 -0
- grag/mcp_server/server.py +447 -0
- grag/registry.py +105 -0
- grag/retrieval/__init__.py +1 -0
- grag/retrieval/bench.py +267 -0
- grag/retrieval/context.py +82 -0
- grag/retrieval/polar.py +422 -0
- grag/retrieval/search.py +259 -0
- grag/retrieval/vectors.py +736 -0
- grag/service.py +186 -0
- gragdb-0.1.0.dist-info/METADATA +210 -0
- gragdb-0.1.0.dist-info/RECORD +32 -0
- gragdb-0.1.0.dist-info/WHEEL +4 -0
- gragdb-0.1.0.dist-info/entry_points.txt +2 -0
- gragdb-0.1.0.dist-info/licenses/LICENSE +21 -0
grag/__init__.py
ADDED
grag/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""grag REST API (FastAPI)."""
|
grag/api/main.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""REST API for grag — thin FastAPI layer over GragService.
|
|
2
|
+
|
|
3
|
+
Every endpoint maps 1:1 to a frozen contract model in grag.core.types and to
|
|
4
|
+
an MCP tool of the same payload. The app object is built here; uvicorn is
|
|
5
|
+
started by the CLI, not this module.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import fastapi
|
|
14
|
+
from fastapi import FastAPI, Query, Request
|
|
15
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
16
|
+
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
17
|
+
from fastapi.staticfiles import StaticFiles
|
|
18
|
+
|
|
19
|
+
import grag
|
|
20
|
+
from grag.config import GragConfig
|
|
21
|
+
from grag.core.errors import GragError, NotFoundError, ReadOnlyViolation
|
|
22
|
+
from grag.core.types import (
|
|
23
|
+
ContextRequest,
|
|
24
|
+
ContextResponse,
|
|
25
|
+
DefineSchemaRequest,
|
|
26
|
+
GraphSample,
|
|
27
|
+
IngestRequest,
|
|
28
|
+
IngestResponse,
|
|
29
|
+
MutationSummary,
|
|
30
|
+
QueryRequest,
|
|
31
|
+
QueryResponse,
|
|
32
|
+
SchemaDocument,
|
|
33
|
+
SearchRequest,
|
|
34
|
+
SearchResponse,
|
|
35
|
+
UpsertEdgesRequest,
|
|
36
|
+
UpsertNodesRequest,
|
|
37
|
+
)
|
|
38
|
+
from grag.registry import ServiceRegistry
|
|
39
|
+
from grag.service import GragService
|
|
40
|
+
|
|
41
|
+
_STATIC_DIR = Path(__file__).parent / "static"
|
|
42
|
+
|
|
43
|
+
DB_HEADER = "x-grag-db"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _error_body(message: str, hint: str | None) -> dict:
|
|
47
|
+
return {"error": message, "hint": hint}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_app(config: GragConfig) -> FastAPI:
|
|
51
|
+
@asynccontextmanager
|
|
52
|
+
async def lifespan(app: FastAPI):
|
|
53
|
+
yield
|
|
54
|
+
app.state.registry.close()
|
|
55
|
+
|
|
56
|
+
app = FastAPI(title="grag", version=grag.__version__, lifespan=lifespan)
|
|
57
|
+
app.state.registry = ServiceRegistry(config)
|
|
58
|
+
# Resolve the default lazily and tolerate failure: in multi-db mode with no
|
|
59
|
+
# determinable default (2+ DBs, none named after db_path), registry.get()
|
|
60
|
+
# raises at startup and the server would never come up — taking /api/dbs
|
|
61
|
+
# (discovery) and all explicitly-selected requests down with it. Startup must
|
|
62
|
+
# not depend on a default existing; only a request with no selector does.
|
|
63
|
+
try:
|
|
64
|
+
app.state.service = app.state.registry.get()
|
|
65
|
+
except GragError:
|
|
66
|
+
app.state.service = None
|
|
67
|
+
|
|
68
|
+
app.add_middleware(
|
|
69
|
+
CORSMiddleware,
|
|
70
|
+
allow_origins=["*"],
|
|
71
|
+
allow_credentials=True,
|
|
72
|
+
allow_methods=["*"],
|
|
73
|
+
allow_headers=["*"],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def resolve(request: Request) -> GragService:
|
|
77
|
+
# Single-db mode: db selectors are ignored, never an error.
|
|
78
|
+
if config.db_dir is None:
|
|
79
|
+
return app.state.registry.get()
|
|
80
|
+
name = request.query_params.get("db") or request.headers.get(DB_HEADER)
|
|
81
|
+
if name:
|
|
82
|
+
return app.state.registry.get(name)
|
|
83
|
+
# No selector: use the pre-resolved default if one exists, else surface
|
|
84
|
+
# the registry's ConfigurationError (400) listing available DBs.
|
|
85
|
+
if app.state.service is not None:
|
|
86
|
+
return app.state.service
|
|
87
|
+
return app.state.registry.get()
|
|
88
|
+
|
|
89
|
+
# -- error mapping ---------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
@app.exception_handler(GragError)
|
|
92
|
+
async def grag_error_handler(_: Request, exc: GragError) -> JSONResponse:
|
|
93
|
+
if isinstance(exc, NotFoundError):
|
|
94
|
+
status = 404
|
|
95
|
+
elif isinstance(exc, ReadOnlyViolation):
|
|
96
|
+
status = 403
|
|
97
|
+
else:
|
|
98
|
+
status = 400
|
|
99
|
+
return JSONResponse(
|
|
100
|
+
status_code=status, content=_error_body(exc.message, exc.hint)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@app.exception_handler(Exception)
|
|
104
|
+
async def unhandled_error_handler(_: Request, exc: Exception) -> JSONResponse:
|
|
105
|
+
return JSONResponse(
|
|
106
|
+
status_code=500, content=_error_body(str(exc), None)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# -- endpoints (contract: see grag.core.types docstring) --------------------
|
|
110
|
+
|
|
111
|
+
@app.get("/api/health")
|
|
112
|
+
def health() -> dict:
|
|
113
|
+
return {"status": "ok", "version": grag.__version__}
|
|
114
|
+
|
|
115
|
+
@app.get("/api/dbs")
|
|
116
|
+
def list_dbs() -> dict:
|
|
117
|
+
default = None
|
|
118
|
+
if config.db_dir is not None and app.state.service is not None:
|
|
119
|
+
# The default service was resolved by the registry at startup, so
|
|
120
|
+
# its db_path is the resolved default .lbdb file. None when no
|
|
121
|
+
# default could be determined (2+ DBs, none preferred).
|
|
122
|
+
default = app.state.service.config.db_path.stem
|
|
123
|
+
return {"dbs": app.state.registry.list_dbs(), "default": default}
|
|
124
|
+
|
|
125
|
+
@app.get("/api/schema")
|
|
126
|
+
def describe_schema(request: Request, format: str | None = Query(default=None)):
|
|
127
|
+
doc = resolve(request).describe_schema()
|
|
128
|
+
if format == "text":
|
|
129
|
+
return PlainTextResponse(doc.text)
|
|
130
|
+
return doc
|
|
131
|
+
|
|
132
|
+
@app.post("/api/schema/define", response_model=SchemaDocument)
|
|
133
|
+
def define_schema(request: Request, req: DefineSchemaRequest) -> SchemaDocument:
|
|
134
|
+
return resolve(request).define_schema(req)
|
|
135
|
+
|
|
136
|
+
@app.post("/api/nodes/upsert", response_model=MutationSummary)
|
|
137
|
+
def upsert_nodes(request: Request, req: UpsertNodesRequest) -> MutationSummary:
|
|
138
|
+
return resolve(request).upsert_nodes(req)
|
|
139
|
+
|
|
140
|
+
@app.post("/api/edges/upsert", response_model=MutationSummary)
|
|
141
|
+
def upsert_edges(request: Request, req: UpsertEdgesRequest) -> MutationSummary:
|
|
142
|
+
return resolve(request).upsert_edges(req)
|
|
143
|
+
|
|
144
|
+
@app.post("/api/query", response_model=QueryResponse)
|
|
145
|
+
def cypher_query(request: Request, req: QueryRequest) -> QueryResponse:
|
|
146
|
+
return resolve(request).cypher_query(req)
|
|
147
|
+
|
|
148
|
+
@app.post("/api/search", response_model=SearchResponse)
|
|
149
|
+
def search_knowledge(request: Request, req: SearchRequest) -> SearchResponse:
|
|
150
|
+
return resolve(request).search_knowledge(req)
|
|
151
|
+
|
|
152
|
+
@app.post("/api/context", response_model=ContextResponse)
|
|
153
|
+
def get_context(request: Request, req: ContextRequest) -> ContextResponse:
|
|
154
|
+
return resolve(request).get_context(req)
|
|
155
|
+
|
|
156
|
+
@app.post("/api/ingest", response_model=IngestResponse)
|
|
157
|
+
def ingest(request: Request, req: IngestRequest) -> IngestResponse:
|
|
158
|
+
return resolve(request).ingest(req)
|
|
159
|
+
|
|
160
|
+
@app.get("/api/graph/sample", response_model=GraphSample)
|
|
161
|
+
def graph_sample(
|
|
162
|
+
request: Request,
|
|
163
|
+
limit: int = Query(default=200),
|
|
164
|
+
label: str | None = Query(default=None),
|
|
165
|
+
) -> GraphSample:
|
|
166
|
+
return resolve(request).graph_sample(limit=limit, label=label)
|
|
167
|
+
|
|
168
|
+
# -- UI statics --------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
if (_STATIC_DIR / "index.html").is_file():
|
|
171
|
+
from fastapi.responses import FileResponse
|
|
172
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
173
|
+
|
|
174
|
+
@app.exception_handler(StarletteHTTPException)
|
|
175
|
+
async def spa_fallback(_: Request, exc: StarletteHTTPException):
|
|
176
|
+
# Unknown non-API GET paths serve the SPA (deep links); real API
|
|
177
|
+
# misses and non-GETs keep their 404.
|
|
178
|
+
request = _
|
|
179
|
+
if (
|
|
180
|
+
exc.status_code == 404
|
|
181
|
+
and request.method == "GET"
|
|
182
|
+
and not request.url.path.startswith("/api/")
|
|
183
|
+
):
|
|
184
|
+
return FileResponse(_STATIC_DIR / "index.html")
|
|
185
|
+
return JSONResponse(
|
|
186
|
+
status_code=exc.status_code, content={"detail": exc.detail}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
app.mount(
|
|
190
|
+
"/",
|
|
191
|
+
StaticFiles(directory=_STATIC_DIR, html=True),
|
|
192
|
+
name="ui",
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
|
|
196
|
+
@app.get("/")
|
|
197
|
+
def root() -> dict:
|
|
198
|
+
return {
|
|
199
|
+
"service": "grag",
|
|
200
|
+
"version": grag.__version__,
|
|
201
|
+
"ui": "not built — see ui/",
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return app
|