feat(graphiti): Implement Graphiti client and related steps for episode ingestion and querying

- Added `graphiti_client.py` to manage the Graphiti client as a singleton.
- Created `ingest_episode_event_step.py` to handle episode ingestion from HTTP webhook events.
- Implemented `ingest_episode_step.py` for validating and enqueuing episode payloads via a POST request.
- Developed `query_graph_step.py` for performing semantic searches in the Graphiti Knowledge-Graph.
- Introduced an `__init__.py` file for the graphiti steps module.
This commit is contained in:
bsiggel
2026-03-30 08:25:49 +00:00
parent 1271e38f2d
commit e255ae1263
8 changed files with 567 additions and 1 deletions

100
services/graphiti_client.py Normal file
View File

@@ -0,0 +1,100 @@
"""Graphiti Knowledge-Graph zentraler Client-Singleton.
Wird von ingest_episode_event_step und query_graph_step genutzt.
Lazy-initialisiert beim ersten Aufruf.
"""
import asyncio
import os
from typing import Any, Optional
from graphiti_core import Graphiti
from graphiti_core.llm_client import LLMConfig
from graphiti_core.llm_client.openai_generic_client import OpenAIGenericClient
from graphiti_core.embedder import OpenAIEmbedder, OpenAIEmbedderConfig
class GraphitiError(Exception):
"""Fehler beim Zugriff auf den Graphiti-Client."""
_graphiti_client: Graphiti | None = None
_graphiti_init_lock = asyncio.Lock()
async def get_graphiti(ctx: Optional[Any] = None) -> Graphiti:
"""Gibt den gecachten Graphiti-Client zurück (Singleton).
Args:
ctx: Optionaler Motia-Context für Logging während der Initialisierung.
Benötigte Umgebungsvariablen:
NEO4J_URI bolt://host:7687
NEO4J_USER neo4j
NEO4J_PASSWORD Pflicht
XAI_API_KEY xAI-Key für LLM (grok)
XAI_BASE_URL optional, Standard: https://api.x.ai/v1
XAI_MODEL optional, Standard: grok-4-1-fast-reasoning
OPENAI_API_KEY OpenAI-Key für Embeddings
GRAPHITI_EMBED_BASE_URL optional, Standard: https://api.openai.com/v1
GRAPHITI_EMBED_MODEL optional, Standard: text-embedding-3-small
"""
global _graphiti_client
if _graphiti_client is None:
async with _graphiti_init_lock:
if _graphiti_client is None:
_log(ctx, "Initialisiere Graphiti-Client...")
try:
_graphiti_client = await _build_graphiti()
_log(ctx, "Graphiti-Client bereit.")
except KeyError as e:
raise GraphitiError(f"Konfigurationsfehler Umgebungsvariable fehlt: {e}") from e
return _graphiti_client
def _log(ctx: Optional[Any], message: str, level: str = "info") -> None:
"""Loggt via ctx.logger falls vorhanden, sonst print."""
if ctx is not None and hasattr(ctx, "logger"):
getattr(ctx.logger, level)(f"[GraphitiClient] {message}")
else:
print(f"[GraphitiClient] {message}")
async def _build_graphiti() -> Graphiti:
neo4j_uri = os.environ["NEO4J_URI"]
neo4j_user = os.environ.get("NEO4J_USER", "neo4j")
neo4j_password = os.environ["NEO4J_PASSWORD"]
xai_base_url = os.environ.get("XAI_BASE_URL", "https://api.x.ai/v1")
xai_api_key = os.environ["XAI_API_KEY"]
xai_model = os.environ.get("XAI_MODEL", "grok-4-1-fast-reasoning")
embed_api_key = os.environ.get("OPENAI_API_KEY") or os.environ.get("GRAPHITI_EMBED_API_KEY", xai_api_key)
embed_base_url = os.environ.get("GRAPHITI_EMBED_BASE_URL", "https://api.openai.com/v1")
embed_model = os.environ.get("GRAPHITI_EMBED_MODEL", "text-embedding-3-small")
llm_client = OpenAIGenericClient(
config=LLMConfig(
api_key=xai_api_key,
base_url=xai_base_url,
model=xai_model,
)
)
embedder = OpenAIEmbedder(
config=OpenAIEmbedderConfig(
api_key=embed_api_key,
base_url=embed_base_url,
embedding_model=embed_model,
embedding_dim=1536,
)
)
client = Graphiti(
uri=neo4j_uri,
user=neo4j_user,
password=neo4j_password,
llm_client=llm_client,
embedder=embedder,
)
await client.build_indices_and_constraints()
return client