Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 30 additions & 194 deletions autobot-backend/knowledge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,206 +34,19 @@

# Store fact
result = await kb.store_fact("Content", {"category": "general"})
"""

import asyncio
import logging
import threading
from typing import Optional

from knowledge.base import KnowledgeBaseCore
from knowledge.bulk import BulkOperationsMixin
from knowledge.categories import CategoriesMixin
from knowledge.collections import CollectionsMixin
from knowledge.documents import DocumentsMixin
from knowledge.facts import FactsMixin
from knowledge.index import IndexMixin
from knowledge.metadata import MetadataMixin
from knowledge.relations import RelationsMixin
from knowledge.search import SearchMixin
from knowledge.stats import StatsMixin
from knowledge.suggestions import SuggestionsMixin
from knowledge.tags import TagsMixin
from knowledge.versioning import VersioningMixin

logger = logging.getLogger(__name__)


class KnowledgeBase(
KnowledgeBaseCore,
StatsMixin,
IndexMixin,
SearchMixin,
FactsMixin,
DocumentsMixin,
TagsMixin,
CategoriesMixin,
CollectionsMixin,
SuggestionsMixin,
MetadataMixin,
VersioningMixin,
BulkOperationsMixin,
RelationsMixin,
):
"""
Unified Knowledge Base implementation.

This class composes all knowledge base functionality through multiple mixins,
providing a complete API for:
- Fact storage and retrieval
- Semantic and keyword search
- Document processing
- Tag management
- Bulk operations
- Statistics and monitoring
- Index management

The class uses Method Resolution Order (MRO) to properly inherit from all mixins,
with KnowledgeBaseCore providing the base initialization and configuration.

Example:
kb = KnowledgeBase()
await kb.initialize()

# Store a fact
result = await kb.store_fact(
"Python uses indentation for blocks",
metadata={"category": "programming", "tags": ["python", "syntax"]}
)

# Search
results = await kb.search("Python syntax", top_k=5)

# Get stats
stats = await kb.get_stats()
"""

def __init__(self):
"""
Initialize the composed knowledge base.

This calls the base __init__ from KnowledgeBaseCore which sets up
all instance variables that are shared across mixins.
"""
super().__init__()
logger.debug("KnowledgeBase instance created (composed from 14 mixins)")

async def initialize(self) -> bool:
"""
Initialize the knowledge base asynchronously.

This is the main initialization method that must be called after construction.
It delegates to KnowledgeBaseCore.initialize() which handles:
- Redis connection setup
- LlamaIndex configuration
- ChromaDB vector store initialization
- Stats counter initialization

Returns:
bool: True if initialization succeeds, False otherwise

Example:
kb = KnowledgeBase()
success = await kb.initialize()
if success:
logger.info("Knowledge base ready")
"""
# Call the base class initialize which sets up everything
success = await super().initialize()

if success:
# Additional initialization can go here if needed
# For now, stats initialization is handled in KnowledgeBaseCore
await self._initialize_stats_counters()

return success


# ============================================================================
# FACTORY FUNCTION - Preferred way to get KnowledgeBase instance
# ============================================================================

_knowledge_base_instance: Optional[KnowledgeBase] = None
_initialization_lock = asyncio.Lock()
_reset_lock = threading.Lock() # Thread-safe reset (Issue #613)


async def get_knowledge_base(force_new: bool = False) -> KnowledgeBase:
"""
Get or create the singleton knowledge base instance (async factory).

This is the preferred way to obtain a knowledge base instance. It ensures
that only one instance exists (singleton pattern) and that it's properly
initialized before being returned.

Args:
force_new: If True, create a new instance even if one exists

Returns:
KnowledgeBase: Fully initialized knowledge base instance

Raises:
RuntimeError: If initialization fails

Example:
# Get the knowledge base (will initialize on first call)
kb = await get_knowledge_base()

# Now ready to use
results = await kb.search("machine learning")
"""
global _knowledge_base_instance

async with _initialization_lock:
if force_new or _knowledge_base_instance is None:
logger.info("Creating new KnowledgeBase instance...")
kb = KnowledgeBase()

# Initialize asynchronously
success = await kb.initialize()

if not success:
raise RuntimeError("Failed to initialize knowledge base")

_knowledge_base_instance = kb
logger.info("KnowledgeBase singleton instance created and initialized")

return _knowledge_base_instance


def reset_knowledge_base() -> None:
"""
Reset the singleton knowledge base instance (thread-safe).

This is primarily useful for testing or when you need to force
reinitialization of the knowledge base.

Note: This does not close existing connections. Call kb.close() first
if you need to properly cleanup resources.

Issue #613: Uses thread-safe locking to prevent race conditions.

Example:
kb = await get_knowledge_base()
await kb.close() # Cleanup resources
reset_knowledge_base() # Reset singleton
kb = await get_knowledge_base() # Get fresh instance
"""
global _knowledge_base_instance
with _reset_lock:
_knowledge_base_instance = None
logger.info("KnowledgeBase singleton instance reset")


# ============================================================================
# EXPORTS
# ============================================================================
Lazy Loading (#1514):
The ``knowledge/__init__.py`` module body no longer eagerly imports
``knowledge.base`` and its mixin siblings (which pull in redis,
llama_index, and chromadb). All heavy classes are deferred to
``knowledge/_composed.py`` and loaded on first attribute access
via PEP 562 ``__getattr__``.
"""

__all__ = [
"KnowledgeBase",
"get_knowledge_base",
"reset_knowledge_base",
# Also export individual mixins for advanced use cases
"KnowledgeBaseCore",
"StatsMixin",
"IndexMixin",
Expand All @@ -249,3 +62,26 @@ def reset_knowledge_base() -> None:
"BulkOperationsMixin",
"RelationsMixin",
]


def __getattr__(name: str):
"""Lazy-load heavy knowledge base classes on first access (#1514).

``__all__`` is the single source of truth for exported names.
Each name is resolved via ``getattr(knowledge._composed, name)``
and cached in module globals so subsequent accesses skip this
function.

Note: if a sibling mixin module (knowledge/base.py, etc.) ever
adds a top-level ``from knowledge import X``, it will create a
circular import through _composed.py. Keep such imports inside
function bodies.
"""
if name not in __all__:
raise AttributeError(f"module 'knowledge' has no attribute {name!r}")

from knowledge import _composed

value = getattr(_composed, name)
globals()[name] = value
return value
96 changes: 96 additions & 0 deletions autobot-backend/knowledge/_composed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# AutoBot - AI-Powered Automation Platform
# Copyright (c) 2025 mrveiss
# Author: mrveiss
"""
Composed KnowledgeBase class and singleton factory (#1514).

This module holds the heavy imports (redis, llama_index, chromadb) that
are triggered by the mixin chain. ``knowledge/__init__.py`` lazily
imports from here so that ``knowledge.pipeline.*`` can be used without
pulling in the full dependency tree.
"""

import asyncio
import logging
import threading
from typing import Optional

from knowledge.base import KnowledgeBaseCore
from knowledge.bulk import BulkOperationsMixin
from knowledge.categories import CategoriesMixin
from knowledge.collections import CollectionsMixin
from knowledge.documents import DocumentsMixin
from knowledge.facts import FactsMixin
from knowledge.index import IndexMixin
from knowledge.metadata import MetadataMixin
from knowledge.relations import RelationsMixin
from knowledge.search import SearchMixin
from knowledge.stats import StatsMixin
from knowledge.suggestions import SuggestionsMixin
from knowledge.tags import TagsMixin
from knowledge.versioning import VersioningMixin

logger = logging.getLogger(__name__)


class KnowledgeBase(
KnowledgeBaseCore,
StatsMixin,
IndexMixin,
SearchMixin,
FactsMixin,
DocumentsMixin,
TagsMixin,
CategoriesMixin,
CollectionsMixin,
SuggestionsMixin,
MetadataMixin,
VersioningMixin,
BulkOperationsMixin,
RelationsMixin,
):
"""Unified Knowledge Base composed from 14 specialised mixins."""

def __init__(self):
super().__init__()
logger.debug("KnowledgeBase instance created (composed from 14 mixins)")

async def initialize(self) -> bool:
"""Initialize the knowledge base asynchronously."""
success = await super().initialize()
if success:
await self._initialize_stats_counters()
return success


# Singleton factory
_knowledge_base_instance: Optional[KnowledgeBase] = None
_initialization_lock = asyncio.Lock()
_reset_lock = threading.Lock()


async def get_knowledge_base(
force_new: bool = False,
) -> KnowledgeBase:
"""Get or create the singleton KnowledgeBase instance."""
global _knowledge_base_instance

async with _initialization_lock:
if force_new or _knowledge_base_instance is None:
logger.info("Creating new KnowledgeBase instance...")
kb = KnowledgeBase()
success = await kb.initialize()
if not success:
raise RuntimeError("Failed to initialize knowledge base")
_knowledge_base_instance = kb
logger.info("KnowledgeBase singleton instance created and initialized")

return _knowledge_base_instance


def reset_knowledge_base() -> None:
"""Reset the singleton (thread-safe, Issue #613)."""
global _knowledge_base_instance
with _reset_lock:
_knowledge_base_instance = None
logger.info("KnowledgeBase singleton instance reset")
Loading