File size: 4,786 Bytes
ec6d5f9
 
 
 
 
 
 
 
 
 
 
 
3cf99bc
ec6d5f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3cf99bc
ec6d5f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
from neo4j import GraphDatabase, Driver, exceptions
from .config import settings
import logging
from typing import List, Dict, Any, Optional

logger = logging.getLogger(__name__)

class Neo4jClient:
    _driver: Optional[Driver] = None

    def _get_driver(self) -> Driver:
        """Initializes and returns the Neo4j driver instance."""
        if self._driver is None or self._driver.close():
            logger.info(f"Initializing Neo4j Driver for URI: {settings.neo4j_uri}")
            try:
                self._driver = GraphDatabase.driver(
                    settings.neo4j_uri,
                    auth=(settings.neo4j_username, settings.neo4j_password.get_secret_value())
                )
                # Verify connectivity during initialization
                self._driver.verify_connectivity()
                logger.info("Neo4j Driver initialized and connection verified.")
            except exceptions.AuthError as e:
                logger.error(f"Neo4j Authentication Error: {e}", exc_info=True)
                raise ConnectionError("Neo4j Authentication Failed. Check credentials.") from e
            except exceptions.ServiceUnavailable as e:
                logger.error(f"Neo4j Service Unavailable: {e}", exc_info=True)
                raise ConnectionError(f"Could not connect to Neo4j at {settings.neo4j_uri}. Ensure DB is running and reachable.") from e
            except Exception as e:
                logger.error(f"Unexpected error initializing Neo4j Driver: {e}", exc_info=True)
                raise ConnectionError("An unexpected error occurred connecting to Neo4j.") from e
        return self._driver

    def close(self):
        """Closes the Neo4j driver connection."""
        if self._driver and not self._driver.close():
            logger.info("Closing Neo4j Driver.")
            self._driver.close()
            self._driver = None

    def query(self, cypher_query: str, params: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
        """Executes a Cypher query and returns the results."""
        driver = self._get_driver()
        logger.debug(f"Executing Cypher: {cypher_query} with params: {params}")
        try:
            # Use session/transaction for robust execution
            with driver.session() as session:
                result = session.run(cypher_query, params or {})
                # Convert Neo4j Records to dictionaries
                data = [record.data() for record in result]
                logger.debug(f"Query returned {len(data)} records.")
                return data
        except (exceptions.ServiceUnavailable, exceptions.SessionExpired) as e:
            logger.error(f"Neo4j connection error during query: {e}", exc_info=True)
            # Attempt to close the potentially broken driver so it reconnects next time
            self.close()
            raise ConnectionError("Neo4j connection error during query execution.") from e
        except exceptions.CypherSyntaxError as e:
            logger.error(f"Neo4j Cypher Syntax Error: {e}\nQuery: {cypher_query}", exc_info=True)
            raise ValueError("Invalid Cypher query syntax.") from e
        except Exception as e:
            logger.error(f"Unexpected error during Neo4j query: {e}", exc_info=True)
            raise RuntimeError("An unexpected error occurred during the Neo4j query.") from e

    def get_schema(self, force_refresh: bool = False) -> Dict[str, Any]:
         """ Fetches the graph schema. Placeholder - Langchain community graph has better schema fetching."""
         # For simplicity, returning empty. Implement actual schema fetching if needed.
         # Consider using langchain_community.graphs.Neo4jGraph for schema handling if complex interactions are needed.
         logger.warning("Neo4jClient.get_schema() is a placeholder. Implement if schema needed.")
         return {} # Placeholder

    def get_concepts(self) -> List[str]:
        """Fetches all Concept names from the graph."""
        cypher = "MATCH (c:Concept) RETURN c.name AS name ORDER BY name"
        results = self.query(cypher)
        return [record['name'] for record in results if 'name' in record]

    def get_concept_description(self, concept_name: str) -> Optional[str]:
         """Fetches the description for a specific concept."""
         cypher = "MATCH (c:Concept {name: $name}) RETURN c.description AS description LIMIT 1"
         params = {"name": concept_name}
         results = self.query(cypher, params)
         return results[0]['description'] if results and 'description' in results[0] else None


# Create a single instance for the application to use
neo4j_client = Neo4jClient()

# Ensure the client is closed gracefully when the application exits
import atexit
atexit.register(neo4j_client.close)