diff --git a/backend/app/graph/graphiti_backend.py b/backend/app/graph/graphiti_backend.py index b27b8060..ad460c3a 100644 --- a/backend/app/graph/graphiti_backend.py +++ b/backend/app/graph/graphiti_backend.py @@ -25,6 +25,28 @@ def _neo4j_val(v: Any) -> Any: return v +def _flatten_attributes(attrs: dict) -> dict: + """Flatten node.attributes so every value is a Neo4j-safe primitive. + + Graphiti extracts entity attributes via a Pydantic model, but the raw LLM + response sometimes wraps each value in a nested dict (e.g. {"value": "CTTI"}). + Neo4j only accepts primitive types or arrays thereof, so we coerce any + dict/list value to its string representation. + """ + result = {} + for k, v in attrs.items(): + if v is None: + continue + if isinstance(v, dict): + # Unwrap {"value": "..."} pattern emitted by some LLMs; fall back to str() + result[k] = v.get("value") or v.get("text") or str(v) + elif isinstance(v, list): + result[k] = ", ".join(str(i) for i in v) + else: + result[k] = v + return result + + def _neo4j_props(node_or_rel: Any) -> Dict[str, Any]: """Return a JSON-safe dict of a Neo4j node or relationship's properties.""" return {k: _neo4j_val(v) for k, v in dict(node_or_rel).items()} @@ -69,7 +91,19 @@ def _make_azure_generic_client(config, client): max_completion_tokens=max_tokens, response_format=response_format, ) - return json.loads(response.choices[0].message.content or '{}') + raw = json.loads(response.choices[0].message.content or '{}') + if response_model is not None: + # Validate through the Pydantic model and dump back to a flat dict + # so that Neo4j never receives nested Maps as property values. + try: + raw = response_model.model_validate(raw).model_dump( + mode='python', exclude_none=True + ) + # Coerce any remaining non-primitive values to str + raw = _flatten_attributes(raw) + except Exception: + pass + return raw except _openai.RateLimitError as e: raise _RateLimitError from e @@ -176,7 +210,7 @@ class GraphitiBackend(GraphBackend): client=async_embed_client, ) cross_encoder = OpenAIRerankerClient(config=llm_config, client=async_small_client) - return Graphiti( + client = Graphiti( uri=self._uri, user=self._user, password=self._password, @@ -184,6 +218,7 @@ class GraphitiBackend(GraphBackend): embedder=embedder, cross_encoder=cross_encoder, ) + return client def create_graph(self, graph_id: str, name: str, description: str = "") -> None: logger.info(f"Graphiti graph namespace ready: {graph_id}") diff --git a/frontend/src/components/GraphPanel.vue b/frontend/src/components/GraphPanel.vue index db188298..ddf815b6 100644 --- a/frontend/src/components/GraphPanel.vue +++ b/frontend/src/components/GraphPanel.vue @@ -77,10 +77,12 @@
Properties:
-
- {{ key }}: - {{ value || 'None' }} -
+
@@ -288,8 +290,10 @@ const entityTypes = computed(() => { // 美观的颜色调色板 const colors = ['#FF6B35', '#004E89', '#7B2D8E', '#1A936F', '#C5283D', '#E9724C', '#3498db', '#9b59b6', '#27ae60', '#f39c12'] - props.graphData.nodes.forEach(node => { - const type = node.labels?.find(l => l !== 'Entity') || 'Entity' + const HIDDEN = new Set(['Episodic', 'Community', 'EpisodicEdge']) + const SKIP = new Set(['Entity', 'Episodic', 'Community']) + props.graphData.nodes.filter(n => !n.labels?.some(l => HIDDEN.has(l))).forEach(node => { + const type = node.labels?.find(l => !SKIP.has(l)) || 'Entity' if (!typeMap[type]) { typeMap[type] = { name: type, count: 0, color: colors[Object.keys(typeMap).length % colors.length] } } @@ -344,19 +348,23 @@ const renderGraph = () => { svg.selectAll('*').remove() - const nodesData = props.graphData.nodes || [] + const HIDDEN_LABELS = new Set(['Episodic', 'Community', 'EpisodicEdge']) + const nodesData = (props.graphData.nodes || []).filter( + n => !n.labels?.some(l => HIDDEN_LABELS.has(l)) + ) const edgesData = props.graphData.edges || [] - + if (nodesData.length === 0) return // Prep data const nodeMap = {} nodesData.forEach(n => nodeMap[n.uuid] = n) - + + const SKIP_TYPE_LABELS = new Set(['Entity', 'Episodic', 'Community']) const nodes = nodesData.map(n => ({ id: n.uuid, name: n.name || 'Unnamed', - type: n.labels?.find(l => l !== 'Entity') || 'Entity', + type: n.labels?.find(l => !SKIP_TYPE_LABELS.has(l)) || 'Entity', rawData: n }))