From d5b5c2ec149b2fa72d0af8180cc30939685d0c48 Mon Sep 17 00:00:00 2001 From: Aaron Nelson Date: Mon, 27 Apr 2026 18:21:22 +0000 Subject: [PATCH] =?UTF-8?q?Graphiti=20sidecar=20service=20+=20SentenceTran?= =?UTF-8?q?sformer=20embedder=20=E2=80=94=20self-hosted,=20no=20OpenAI=20d?= =?UTF-8?q?ependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/graphiti_service.py | 133 ++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 scripts/graphiti_service.py diff --git a/scripts/graphiti_service.py b/scripts/graphiti_service.py new file mode 100644 index 0000000..34ce287 --- /dev/null +++ b/scripts/graphiti_service.py @@ -0,0 +1,133 @@ +""" +Aaron AI — Graphiti Sidecar Service +Wraps graphiti-core in a FastAPI service to avoid asyncio event loop conflicts. +Port 8001 (internal only). No OpenAI dependency. +""" + +import os, logging, sys +from contextlib import asynccontextmanager +from datetime import datetime +from pathlib import Path + +from dotenv import load_dotenv +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +load_dotenv(Path.home() / "aaronai" / ".env") + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") +log = logging.getLogger("graphiti-sidecar") + +GROUP_ID = os.getenv("GRAPHITI_GROUP_ID", "aaron") +FALKORDB_HOST = os.getenv("FALKORDB_HOST", "localhost") +FALKORDB_PORT = int(os.getenv("FALKORDB_PORT", "6379")) +LLM_PROVIDER = os.getenv("LLM_PROVIDER", "anthropic") +LLM_MODEL = os.getenv("LLM_MODEL", "claude-sonnet-4-6") +LLM_API_KEY = os.getenv("LLM_API_KEY") or os.getenv("ANTHROPIC_API_KEY") +os.environ["EMBEDDING_DIM"] = "384" + +def get_llm_client(): + from graphiti_core.llm_client.config import LLMConfig + config = LLMConfig(api_key=LLM_API_KEY, model=LLM_MODEL) + if LLM_PROVIDER == "anthropic": + from graphiti_core.llm_client.anthropic_client import AnthropicClient + return AnthropicClient(config) + elif LLM_PROVIDER == "openai": + from graphiti_core.llm_client.openai_client import OpenAIClient + return OpenAIClient(config) + elif LLM_PROVIDER == "gemini": + from graphiti_core.llm_client.gemini_client import GeminiClient + return GeminiClient(config) + elif LLM_PROVIDER == "groq": + from graphiti_core.llm_client.groq_client import GroqClient + return GroqClient(config) + raise ValueError(f"Unsupported LLM provider: {LLM_PROVIDER}") + +graphiti_instance = None + +async def get_graphiti(): + if graphiti_instance is None: + raise HTTPException(status_code=503, detail="Graphiti not initialized") + return graphiti_instance + +@asynccontextmanager +async def lifespan(app: FastAPI): + global graphiti_instance + sys.path.insert(0, str(Path.home() / "aaronai" / "scripts")) + log.info("Loading embedding and reranker models...") + from st_embedder import SentenceTransformerEmbedder + from graphiti_core.cross_encoder.bge_reranker_client import BGERerankerClient + from graphiti_core.driver.falkordb_driver import FalkorDriver + from graphiti_core import Graphiti + log.info(f"Connecting to FalkorDB at {FALKORDB_HOST}:{FALKORDB_PORT}...") + graphiti_instance = Graphiti( + llm_client=get_llm_client(), + embedder=SentenceTransformerEmbedder(), + cross_encoder=BGERerankerClient(), + graph_driver=FalkorDriver(host=FALKORDB_HOST, port=FALKORDB_PORT), + ) + await graphiti_instance.build_indices_and_constraints() + log.info(f"Graphiti ready — provider: {LLM_PROVIDER}, group: {GROUP_ID}") + yield + await graphiti_instance.close() + +app = FastAPI(title="Aaron AI Graphiti Sidecar", lifespan=lifespan) + +class EpisodeRequest(BaseModel): + name: str + content: str + source_description: str = "" + timestamp: str | None = None + group_id: str | None = None + +@app.get("/health") +async def health(): + return {"ok": True, "provider": LLM_PROVIDER, "group": GROUP_ID} + +@app.post("/episodes") +async def add_episode(req: EpisodeRequest): + g = await get_graphiti() + from graphiti_core.nodes import EpisodeType + try: + ref_time = datetime.fromisoformat(req.timestamp) if req.timestamp else datetime.now() + await g.add_episode( + name=req.name, + episode_body=req.content, + source=EpisodeType.text, + reference_time=ref_time, + source_description=req.source_description, + group_id=req.group_id or GROUP_ID, + ) + return {"ok": True} + except Exception as e: + log.error(f"Episode ingestion failed: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/search") +async def search(query: str, limit: int = 8, group_id: str | None = None): + g = await get_graphiti() + try: + results = await g.search( + query=query, + num_results=limit, + group_ids=[group_id or GROUP_ID], + ) + return { + "results": [ + { + "fact": r.fact, + "source": getattr(r, "source_node_uuid", ""), + "score": getattr(r, "score", 0), + "valid_at": str(getattr(r, "valid_at", "")), + "invalid_at": str(getattr(r, "invalid_at", "")), + } + for r in results + ] + } + except Exception as e: + log.error(f"Search failed: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="127.0.0.1", port=8001, log_level="info")