265 lines
10 KiB
Python
265 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Optional
|
|
|
|
from flask import Flask, jsonify, request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from graphiti_core.driver.falkordb_driver import FalkorDriver
|
|
from graphiti_core.embedder.openai import OpenAIEmbedder, OpenAIEmbedderConfig
|
|
from graphiti_core.graphiti import Graphiti
|
|
from graphiti_core.llm_client.config import LLMConfig
|
|
from graphiti_core.llm_client.openai_client import OpenAIClient
|
|
from graphiti_core.nodes import EpisodeType
|
|
|
|
app = Flask(__name__)
|
|
|
|
FALKORDB_HOST = os.environ.get("FALKORDB_HOST", "graphiti-falkordb")
|
|
FALKORDB_PORT = int(os.environ.get("FALKORDB_PORT", "6379"))
|
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") or os.environ.get("LLM_API_KEY")
|
|
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL") or os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1")
|
|
MODEL_NAME = os.environ.get("MODEL_NAME") or os.environ.get("GRAPHITI_MODEL_NAME", "gpt-5.4-mini")
|
|
EMBEDDING_MODEL_NAME = os.environ.get("EMBEDDING_MODEL_NAME") or os.environ.get("GRAPHITI_EMBEDDING_MODEL_NAME", "text-embedding-3-small")
|
|
|
|
ONTOLOGIES: dict[str, dict[str, Any]] = {}
|
|
INDICES_INITIALIZED: set[str] = set()
|
|
RESERVED_NAMES = {"uuid", "name", "group_id", "name_embedding", "summary", "created_at"}
|
|
|
|
|
|
def run(coro):
|
|
return asyncio.run(coro)
|
|
|
|
|
|
def safe_attr_name(attr_name: str) -> str:
|
|
if attr_name.lower() in RESERVED_NAMES:
|
|
return f"entity_{attr_name}"
|
|
return attr_name
|
|
|
|
|
|
def build_ontology(ontology: dict[str, Any]) -> dict[str, Any]:
|
|
entity_types: dict[str, type[BaseModel]] = {}
|
|
entity_names: list[str] = []
|
|
for entity_def in ontology.get("entity_types", []):
|
|
name = entity_def["name"]
|
|
description = entity_def.get("description", f"A {name} entity.")
|
|
attrs: dict[str, Any] = {"__doc__": description}
|
|
annotations: dict[str, Any] = {}
|
|
for attr_def in entity_def.get("attributes", []):
|
|
attr_name = safe_attr_name(attr_def["name"])
|
|
attrs[attr_name] = Field(default=None, description=attr_def.get("description", attr_name))
|
|
annotations[attr_name] = Optional[str]
|
|
attrs["__annotations__"] = annotations
|
|
entity_types[name] = type(name, (BaseModel,), attrs)
|
|
entity_names.append(name)
|
|
|
|
edge_types: dict[str, type[BaseModel]] = {}
|
|
edge_type_map: dict[tuple[str, str], list[str]] = {}
|
|
for edge_def in ontology.get("edge_types", []):
|
|
name = edge_def["name"]
|
|
description = edge_def.get("description", f"A {name} relationship.")
|
|
edge_types[name] = type(name, (BaseModel,), {"__doc__": description, "__annotations__": {}})
|
|
for st in edge_def.get("source_targets", []):
|
|
source = st.get("source", "Entity")
|
|
target = st.get("target", "Entity")
|
|
edge_type_map.setdefault((source, target), []).append(name)
|
|
|
|
if edge_types and not edge_type_map:
|
|
labels = entity_names + ["Entity"]
|
|
edge_names = list(edge_types.keys())
|
|
edge_type_map = {(source, target): edge_names for source in labels for target in labels}
|
|
|
|
return {"entity_types": entity_types or None, "edge_types": edge_types or None, "edge_type_map": edge_type_map or None}
|
|
|
|
|
|
async def graphiti(graph_id: str) -> Graphiti:
|
|
if not OPENAI_API_KEY:
|
|
raise RuntimeError("OPENAI_API_KEY/LLM_API_KEY is not configured")
|
|
os.environ.setdefault("OPENAI_API_KEY", OPENAI_API_KEY)
|
|
os.environ.setdefault("OPENAI_BASE_URL", OPENAI_BASE_URL)
|
|
os.environ.setdefault("MODEL_NAME", MODEL_NAME)
|
|
os.environ.setdefault("EMBEDDING_MODEL_NAME", EMBEDDING_MODEL_NAME)
|
|
driver = FalkorDriver(host=FALKORDB_HOST, port=FALKORDB_PORT, database=graph_id)
|
|
return Graphiti(graph_driver=driver)
|
|
|
|
|
|
async def ensure_indices(graph_id: str) -> None:
|
|
if graph_id in INDICES_INITIALIZED:
|
|
return
|
|
client = await graphiti(graph_id)
|
|
await client.build_indices_and_constraints()
|
|
INDICES_INITIALIZED.add(graph_id)
|
|
|
|
|
|
async def query_graph(graph_id: str, query: str, **params: Any) -> list[dict[str, Any]]:
|
|
driver = FalkorDriver(host=FALKORDB_HOST, port=FALKORDB_PORT, database=graph_id)
|
|
rows, _, _ = await driver.execute_query(query, **params)
|
|
return rows
|
|
|
|
|
|
def edge_from_obj(edge: Any) -> dict[str, Any]:
|
|
return {
|
|
"uuid": getattr(edge, "uuid", "") or getattr(edge, "uuid_", ""),
|
|
"name": getattr(edge, "name", ""),
|
|
"fact": getattr(edge, "fact", ""),
|
|
"source_node_uuid": getattr(edge, "source_node_uuid", ""),
|
|
"target_node_uuid": getattr(edge, "target_node_uuid", ""),
|
|
"attributes": getattr(edge, "attributes", {}) or {},
|
|
"created_at": str(getattr(edge, "created_at", "") or "") or None,
|
|
"valid_at": str(getattr(edge, "valid_at", "") or "") or None,
|
|
"invalid_at": str(getattr(edge, "invalid_at", "") or "") or None,
|
|
"expired_at": str(getattr(edge, "expired_at", "") or "") or None,
|
|
}
|
|
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
return jsonify({"status": "ok", "service": "graphiti-bridge"})
|
|
|
|
|
|
@app.post("/graphs")
|
|
def create_graph():
|
|
payload = request.get_json(force=True)
|
|
graph_id = payload["graph_id"]
|
|
run(ensure_indices(graph_id))
|
|
return jsonify({"graph_id": graph_id})
|
|
|
|
|
|
@app.post("/graphs/<graph_id>/ontology")
|
|
def set_ontology(graph_id: str):
|
|
ONTOLOGIES[graph_id] = build_ontology(request.get_json(force=True) or {})
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@app.post("/graphs/<graph_id>/episodes")
|
|
def add_episodes(graph_id: str):
|
|
payload = request.get_json(force=True)
|
|
chunks = payload.get("chunks") or [payload.get("text", "")]
|
|
|
|
async def add_all():
|
|
await ensure_indices(graph_id)
|
|
client = await graphiti(graph_id)
|
|
ontology = ONTOLOGIES.get(graph_id, {})
|
|
out = []
|
|
for index, chunk in enumerate(chunks, 1):
|
|
result = await client.add_episode(
|
|
name=f"mirofish-chunk-{index}",
|
|
episode_body=chunk,
|
|
source_description="Mirofish text episode",
|
|
reference_time=datetime.now(timezone.utc),
|
|
source=EpisodeType.text,
|
|
group_id=graph_id,
|
|
entity_types=ontology.get("entity_types"),
|
|
edge_types=ontology.get("edge_types"),
|
|
edge_type_map=ontology.get("edge_type_map"),
|
|
custom_extraction_instructions="Extract actors and relationships relevant to the simulation. Prefer provided ontology labels when supported by the text.",
|
|
)
|
|
episode_uuid = getattr(result.episode, "uuid", None) or getattr(result.episode, "uuid_", None)
|
|
out.append({"uuid": episode_uuid, "processed": True})
|
|
return out
|
|
|
|
return jsonify({"episodes": run(add_all())})
|
|
|
|
|
|
@app.get("/graphs/<graph_id>/episodes/<episode_uuid>")
|
|
def get_episode(graph_id: str, episode_uuid: str):
|
|
return jsonify({"uuid": episode_uuid, "processed": True})
|
|
|
|
|
|
@app.get("/graphs/<graph_id>/nodes")
|
|
def get_nodes(graph_id: str):
|
|
rows = run(query_graph(graph_id, """
|
|
MATCH (n:Entity)
|
|
RETURN n.uuid AS uuid, n.name AS name, labels(n) AS labels, n.summary AS summary, n.created_at AS created_at
|
|
LIMIT 2000
|
|
"""))
|
|
return jsonify({"nodes": rows})
|
|
|
|
|
|
@app.get("/graphs/<graph_id>/edges")
|
|
def get_edges(graph_id: str):
|
|
rows = run(query_graph(graph_id, """
|
|
MATCH (a:Entity)-[r]->(b:Entity)
|
|
RETURN r.uuid AS uuid, type(r) AS name, r.fact AS fact,
|
|
a.uuid AS source_node_uuid, b.uuid AS target_node_uuid,
|
|
r.created_at AS created_at, r.valid_at AS valid_at, r.invalid_at AS invalid_at, r.expired_at AS expired_at
|
|
LIMIT 5000
|
|
"""))
|
|
return jsonify({"edges": rows})
|
|
|
|
|
|
@app.get("/nodes/<node_uuid>")
|
|
def get_node(node_uuid: str):
|
|
graph_id = request.args.get("graph_id")
|
|
if not graph_id:
|
|
return jsonify({"node": None})
|
|
rows = run(query_graph(graph_id, """
|
|
MATCH (n:Entity {uuid: $uuid})
|
|
RETURN n.uuid AS uuid, n.name AS name, labels(n) AS labels, n.summary AS summary, n.created_at AS created_at
|
|
LIMIT 1
|
|
""", uuid=node_uuid))
|
|
return jsonify({"node": rows[0] if rows else None})
|
|
|
|
|
|
@app.get("/nodes/<node_uuid>/edges")
|
|
def get_node_edges(node_uuid: str):
|
|
graph_id = request.args.get("graph_id")
|
|
if not graph_id:
|
|
return jsonify({"edges": []})
|
|
rows = run(query_graph(graph_id, """
|
|
MATCH (a:Entity)-[r]->(b:Entity)
|
|
WHERE a.uuid = $uuid OR b.uuid = $uuid
|
|
RETURN r.uuid AS uuid, type(r) AS name, r.fact AS fact,
|
|
a.uuid AS source_node_uuid, b.uuid AS target_node_uuid,
|
|
r.created_at AS created_at, r.valid_at AS valid_at, r.invalid_at AS invalid_at, r.expired_at AS expired_at
|
|
LIMIT 5000
|
|
""", uuid=node_uuid))
|
|
return jsonify({"edges": rows})
|
|
|
|
|
|
@app.post("/graphs/<graph_id>/search")
|
|
def search(graph_id: str):
|
|
payload = request.get_json(force=True)
|
|
query = payload.get("query", "")
|
|
limit = int(payload.get("limit", 10))
|
|
scope = payload.get("scope", "edges")
|
|
|
|
async def do_search():
|
|
client = await graphiti(graph_id)
|
|
edges = [] if scope == "nodes" else [edge_from_obj(edge) for edge in await client.search(query=query, group_ids=[graph_id], num_results=limit)]
|
|
nodes = []
|
|
if scope in {"nodes", "both"}:
|
|
terms = [term.lower() for term in query.split() if len(term) > 1]
|
|
rows = await query_graph(graph_id, """
|
|
MATCH (n:Entity)
|
|
RETURN n.uuid AS uuid, n.name AS name, labels(n) AS labels, n.summary AS summary, n.created_at AS created_at
|
|
LIMIT 2000
|
|
""")
|
|
scored = []
|
|
for row in rows:
|
|
text = f"{row.get('name', '')} {row.get('summary', '')}".lower()
|
|
score = sum(1 for term in terms if term in text)
|
|
if query.lower() in text:
|
|
score += 10
|
|
if score:
|
|
scored.append((score, row))
|
|
scored.sort(key=lambda item: item[0], reverse=True)
|
|
nodes = [row for _, row in scored[:limit]]
|
|
return {"edges": edges, "nodes": nodes}
|
|
|
|
return jsonify(run(do_search()))
|
|
|
|
|
|
@app.delete("/graphs/<graph_id>")
|
|
def delete_graph(graph_id: str):
|
|
run(query_graph(graph_id, "MATCH (n) DETACH DELETE n"))
|
|
ONTOLOGIES.pop(graph_id, None)
|
|
INDICES_INITIALIZED.discard(graph_id)
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "8008")))
|